Metadata-Version: 2.4
Name: cbpr-usage-rules
Version: 0.5.0
Summary: Validate ISO 20022 CBPR+ XML messages against SWIFT usage (business) rules.
Author: Pete Houghton
License: MIT License
        
        Copyright (c) 2021 Peter Houghton
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/phoughton/iso20022-cbpr-ur
Project-URL: Repository, https://github.com/phoughton/iso20022-cbpr-ur
Project-URL: Issues, https://github.com/phoughton/iso20022-cbpr-ur/issues
Keywords: iso20022,cbpr,swift,payments,validation,pacs,pain,camt
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: lxml>=4.9
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: openpyxl>=3.1; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# cbpr-usage-rules

Validate **ISO 20022 CBPR+** XML messages (the SWIFT cross-border payments
profile) against the **Usage / Business Rules** that apply on top of the XSD
schemas. These are the rules that the schema alone cannot express: cross-field
constraints, cross-schema checks between the Business Application Header (BAH)
and the message Document, conditional presence, code-list restrictions, and
field-format algorithms (IBAN, LEI, BIC, country, currency).

Rules are versioned per year (currently **2025** and **2026**) and organised by
message type (pacs.008, pacs.009 and its COV/ADV variants, pacs.002, pacs.004,
pain.001, camt.052, camt.054, and the STP variants).

> **This package is AI generated.** The rule logic was derived, with review,
> from the published CBPR+ usage-guideline spreadsheets. Treat it as an aid, not
> as a substitute for the official SWIFT specifications. Verify against the
> source material before relying on results in production.

---

## Installation

```bash
pip install cbpr-usage-rules
```

Requires Python 3.9+ and depends only on `lxml`.

---

## Quick start

### As a library

```python
import cbpr_rules

result = cbpr_rules.validate_file("payment.xml", year=2025)

if result["valid"]:
    print("Compliant")
else:
    for v in result["violations"]:
        print(f"{v['rule_number']}  line {v['line']}  {v['xpath']}")
        print(f"    {v['description']}")
```

`validate_string` is the same but takes XML text:

```python
result = cbpr_rules.validate_string(xml_text, year=2026)
```

### From the command line

```bash
# Human-readable (default). Exit code is non-zero if there are violations.
cbpr-validate payment.xml --year 2025

# Compact, one finding per line (compiler-style) — for tools and coding agents
cbpr-validate payment.xml --year 2025 --format compact

# JSON output (full machine-readable result)
cbpr-validate payment.xml --year 2025 --json   # same as --format json

# Read from stdin
cat payment.xml | cbpr-validate --year 2026

# Also list the advisory (non-enforced) rules in full
cbpr-validate payment.xml --year 2025 --advisory

# Additionally schema-validate against an XSD (repeatable, results shown separately)
cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
```

Each reported violation shows the rule, **why** this instance failed (`Problem:`),
the **offending XML** from your file (`Found:`), and its line + xpath (`At:`).

**For tools and coding agents**, prefer `--format compact` (one finding per line,
parseable like a compiler/linter) or `--json` (the full structured result):

```text
$ cbpr-validate payment.xml --year 2025 --format compact
payment.xml:7: violation [pacs.008:R2] From vs Instructing Agent: ABCDGB2LXXX != GGGGGB2LXXX | at /AppHdr/Fr/FIId/FinInstnId/BICFI
payment.xml:63: violation [pacs.008:VAL-IBAN] invalid IBAN: 'GB29NWBK60161331926818' | at /Document/.../DbtrAcct/Id/IBAN
INVALID: 5 violations (87 rules)
```

Each line is `file:line: severity [rule] message | at xpath`, and the final
`VALID:` / `INVALID:` line is a stable summary to grep for. With `--xsd`, schema
errors appear as `schema-error` lines and are counted in the summary.
Advisory rules are summarised as a count by default; pass `--advisory` to list them.

---

## Example messages

For every supported message type the package bundles a **minimum** and a
**maximum** example message — both pre-verified to pass the usage rules *and* the
CBPR+ XSDs:

- **min** — only the mandatory fields (smallest valid message);
- **max** — every XSD-visible field populated (one representative occurrence; the
  richest single legal combination, since some fields are mutually exclusive).

```bash
# Print to stdout (full AppHdr + Document, wrapped in <Envelope>)
cbpr-validate --example max --year 2025 --type pacs.008
cbpr-validate --example min --year 2026 --type pacs.009_cov > template.xml

# Override the wrapper tag (default: Envelope)
cbpr-validate --example max --year 2025 --type pacs.008 --wrapper BusinessMessage
```
```python
import cbpr_rules
xml = cbpr_rules.example_message(2025, "pacs.008", "max")   # variant defaults to "max"
cbpr_rules.example_variants(2025, "pacs.008")               # ['min', 'max']
```

The data is **fictitious** (synthetic, generated BICs/IBANs/LEIs) and does
not reproduce any real sample — use them as starting templates, not real payments.

---

## Generating identifiers

The example values above are produced by a small **deterministic** ID generator
(the "Counting Strings" algorithm — no `random`/`uuid`, same seed → same output).
Every generated id is structurally valid and passes the tool's own validators.

```bash
cbpr-validate --generate iban --country AT --seed s1     # ISO 13616 + mod-97
cbpr-validate --generate lei  --seed acme                # ISO 17442 + check digits
cbpr-validate --generate bic  --bank JSBP --country GB   # -> JSBPGB..XXX
cbpr-validate --generate uuid --count 3                  # RFC 4122 v4 (UETR)
cbpr-validate --generate mid  --country GB               # message identifier
```
```python
from cbpr_rules import generate_iban, generate_lei, generate_bic, generate_uetr
generate_iban("DE", seed="acme")   # deterministic, valid German IBAN
generate_bic(country="GB", seed="acme")
```

IBAN generation supports the 27 EU countries plus GB.

---

## The result object

Both `validate_file` and `validate_string` return a dictionary:

```python
{
  "valid": False,                     # True only if there are no VIOLATION-severity findings
  "message_type": "pacs.008",         # the rule set that was applied
  "detected_message_type": "pacs.008",# auto-detected from the Document namespace
  "year": 2025,
  "rules_evaluated": 87,
  "violations": [
    {
      "rule_number": "pacs.008:R41",   # unique within the message type
      "name": "CBPR_Interbank_Settlement_Currency_FormalRule",
      "description": "The codes XAU, XAG, XPD and XPT are not allowed ...",
      "detail": "commodity currency 'XAU' not allowed",  # why this instance was flagged
      "found": "<IntrBkSttlmAmt Ccy=\"XAU\">1000.00</IntrBkSttlmAmt>",  # the offending XML
      "xpath": "/RequestPayload/Document/FIToFICstmrCdtTrf/CdtTrfTxInf/IntrBkSttlmAmt",
      "line": 39,                      # 1-based line in the source XML
      "severity": "violation"
    }
  ],
  "advisory": [                        # textual guidance that cannot be mechanically enforced
    {"rule_number": "pacs.008:R4", "name": "...", "description": "..."}
  ]
}
```

Every violation carries the four required pieces of information: the **xpath**,
the **line number**, a **description**, and a **unique rule number**.

---

## Mid-level usage

### Choosing the year

Pass `year=2025` or `year=2026`. Rule loading is per-year and lazy — only the
requested year's modules are imported. You can validate against more than one
year in the same process simply by calling with different `year` values.

### Message type detection and variants

The message type is auto-detected from the `<Document>` namespace, so you
normally don't pass it. Business variants that share a base namespace — **STP**
(pacs.008/pacs.009), **COV** and **ADV** (pacs.009) — cannot be told apart from
the XML alone, so select them explicitly when you want their stricter rule sets:

```python
cbpr_rules.validate_file("cover.xml", year=2025, msgtype="pacs.009_cov")
```

```bash
cbpr-validate cover.xml --year 2025 --type pacs.009_cov
```

### Wrapper tags

Messages arrive inside different envelopes (`<RequestPayload>`, `<DataPDU>`,
SWIFT SAA wrappers, and so on). The validator locates the `AppHdr` and
`Document` elements wherever they sit in the tree and ignores the surrounding
wrapper, so the same input validates identically regardless of envelope. Element
matching is by local name, so namespace prefixes never matter.

### Severity

- `violation` — a usage rule is broken; this makes `valid` false.
- `info` — advisory guidance surfaced for awareness; it never fails validation
  and appears in the separate `advisory` list (textual rules that cannot be
  mechanically checked).

---

## In-depth usage

### Discovering the rule set

```python
cbpr_rules.available(2025)              # -> ['camt.052', 'pacs.008', ...]
cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
cbpr_rules.list_rules(2025, "pacs.008", enforced_only=True)  # only the enforceable rules
```

```bash
cbpr-validate --year 2025 --list-types
cbpr-validate --year 2025 --type pacs.008 --list               # all rules
cbpr-validate --year 2025 --type pacs.008 --enforced           # enforceable rules only
```

### Algorithmic field validation

Beyond the published rules, the following formats are validated with their
standard algorithms wherever the corresponding fields appear:

| Field    | Standard            | Check                                   |
|----------|---------------------|-----------------------------------------|
| IBAN     | ISO 13616           | structure + mod-97 check digits         |
| LEI      | ISO 17442           | structure + ISO 7064 mod-97-10 check    |
| BIC      | ISO 9362            | 8/11-char structure + valid country     |
| Country  | ISO 3166-1 alpha-2  | membership                              |
| Currency | ISO 4217            | membership                              |

The reference code lists are vendored, so validation never makes a network call.

Some of these check-digit algorithms and reference code lists were derived from
Wikipedia (e.g. [IBAN](https://en.wikipedia.org/wiki/International_Bank_Account_Number),
[LEI](https://en.wikipedia.org/wiki/Legal_Entity_Identifier), and the
[ISO 3166 country codes](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes)).
Verify against the authoritative ISO/registry sources before relying on them in
production.

### Optional XSD schema validation

You can additionally validate a message against one or more **XSD schemas** as a
*separate* second result set. XSD files are **not** bundled with the package —
supply your own path(s):

```python
result = cbpr_rules.validate_file("payment.xml", year=2025, xsd="pacs.008.001.08.xsd")
# or several:  xsd=["head.001.001.02.xsd", "pacs.008.001.08.xsd"]
```

```bash
cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
```

Each XSD is auto-matched by its `targetNamespace`: a message schema validates the
`Document`, a head.001 schema validates the `AppHdr`. When (and only when) an XSD
is supplied, the result gains a separate top-level `xsd` block, and the CLI prints
a distinct **XSD SCHEMA VALIDATION** section:

```python
"xsd": {
  "checked": True,
  "schema_valid": False,                 # all supplied schemas passed?
  "schemas": [
    {
      "file": "pacs.008.001.08.xsd",
      "target_namespace": "urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08",
      "validated_element": "Document",   # or "AppHdr" / "root"
      "valid": False,
      "errors": [{"message": "...", "line": 82, "xpath": "/Document/.../CdtrAgt"}]
    }
  ]
}
```

Usage-rule results and schema results stay separate: the top-level `valid` reflects
only the usage rules, while schema validity is `xsd.schema_valid`. The **CLI exit
code is non-zero if either** the usage rules or the schema fail. With no `--xsd`,
nothing about XSD appears in the output.

### Handling errors

`validate_file` / `validate_string` raise `cbpr_rules.engine.ValidationError`
for unparseable XML or when the message type cannot be determined and was not
supplied. The CLI turns these into a message on stderr and exit code 2.

### CLI exit codes

| Code | Meaning                          |
|------|----------------------------------|
| 0    | Valid (no violations)            |
| 1    | Invalid (one or more violations) |
| 2    | Usage error / could not validate |

---

## How the rules are organised

Each `(year, message type)` has a hand-authored Python module under
`cbpr_rules/rules/y<year>/`. Rules are built from a small library of reusable
combinators (presence, conditional presence, mutual exclusion, value matching,
length, code lists, address grace-period rules) plus bespoke functions for
cross-field and cross-schema logic. The XSD files in the source material are
informational only and are not reimplemented here.

### Enforced vs advisory

Every published rule is registered. A rule is **enforced** (can produce a
violation) when it can be checked deterministically from the message alone —
this includes the formal pseudo-code rules and the mechanizable textual rules,
such as:

- header ↔ message consistency (`MsgDefIdr` matches the Document definition;
  `BizMsgIdr` carries the GroupHeader `MsgId`);
- no structured Postal Address value duplicated in an `AddressLine`;
- `AnyBIC` present ⇒ `Name`/`PostalAddress` not allowed;
- Structured Remittance ≤ 9,000 characters;
- charge rules where instructed and settlement amounts share a currency and
  differ, and amount-total-equals-sum checks.

A rule stays **advisory** (`severity: info`, surfaced as guidance, never failing
validation) when it cannot be verified from the message in isolation — for
example anything that refers to a *related or underlying message* not present
(`Original_*`, related-BAH, COV/ADV UETR/E2E), depends on the SWIFT network or a
jurisdiction, or expresses a recommendation / bilaterally-agreed practice.
Checks are deliberately **conservative**: each skips when its inputs are absent
or ambiguous, so a compliant message is never failed spuriously.

## Licence

MIT — see [LICENSE](LICENSE).
