pain001.faces package#

Faces: project pain.001 messages into compatible standards with loss reports (ADR-0005).

A face projects a pain.001 payment message into an external industry standard or rail convention: UK Open Banking v4.0.1 domestic payments, Berlin Group NextGenPSD2 v1.3.16 payment initiation, or ISO 20022 element names in snake_case. Every occurrence of every source element is either carried into the payload or listed in an explicit LossReport; a face never invents a value the target requires, it refuses the document with a FaceError.

Submodules#

pain001.faces.berlin_group module#

Berlin Group NextGenPSD2 payment initiation face (ADR-0005).

Target: the JSON request bodies of the NextGenPSD2 XS2A Framework v1.3.16 (2025-11-27; OpenAPI file under CC BY 4.0): a document with one transaction becomes paymentInitiation_json, one with several becomes bulkPaymentInitiation_json with paymentInitiationBulkElement_json items. Only properties those schemas define are produced; the rest of the source (instruction identification, purpose, charge bearer, a single payment’s execution date, …) is left to the loss report. The payment product in the request path is the caller’s choice and is not part of the body.

pain001.faces.berlin_group.project_berlin_group(twin: dict[str, Any], bindings: dict[str, Any] | None = None) → tuple[dict[str, Any], set[str]][source]#

Project a pain.001 twin into a NextGenPSD2 payment initiation body.

Parameters:
  • twin – The ISO JSON twin of the document.

  • bindings – Unused: this face takes no side inputs (the engine refuses any).

Returns:

The request body and the set of source leaf paths it carries.

A document with no transaction or more than one payment information block, or one lacking an element the target requires, is refused with a FaceError.

pain001.faces.cli module#

pain001 face: project a pain.001 file into a face (ADR-0005).

pain001.faces.common module#

Shared navigation for the payment faces (UK Open Banking, Berlin Group).

A face never invents a value: an element the target requires but the source lacks is refused with a FaceError that names it.

pain001.faces.common.account_id(reader: Reader, node: dict[str, Any], path: str, element: str) → tuple[str, str] | None[source]#

("iban", value) or ("bban", value) for an account element.

A BBAN is read only from Id/Othr whose scheme code is BBAN; any other Othr identification has no counterpart and is left to the loss report.

Parameters:
  • reader – The reader recording mapped leaves.

  • node – The node holding the account element.

  • path – The indexed path of node.

  • element – The account element tag (DbtrAcct or CdtrAcct).

Returns:

The identification kind and value, or None.

pain001.faces.common.address(reader: Reader, party: dict[str, Any], path: str, keys: dict[str, str]) → dict[str, Any][source]#

Map a party’s PstlAdr to target keys, recording what is used.

Parameters:
  • reader – The reader recording mapped leaves.

  • party – The party node holding PstlAdr.

  • path – The indexed path of party.

  • keys – Target key for each ISO element name in ADDRESS_TAGS the target supports, plus "AddressLine" when it supports address lines.

Returns:

The target address object, empty when nothing maps.

pain001.faces.common.agent_bic(reader: Reader, node: dict[str, Any], path: str, element: str) → str | None[source]#

The BIC of an agent element (BICFI, or BIC before .04).

pain001.faces.common.payment_information(twin: dict[str, Any], face: str) → tuple[dict[str, Any], str][source]#

The document’s single PmtInf block and its path.

Parameters:
  • twin – The ISO JSON twin.

  • face – The face name, for the error message.

Returns:

The payment information node and its indexed path.

Raises:

FaceError – When the document has no payment information block, or more than one (both faces take one debtor account and one execution date per request).

pain001.faces.common.required(value: Any, face: str, what: str) → Any[source]#

Return value, or refuse when the target needs it and it is absent.

Parameters:
  • value – The projected value, None when the source lacks it.

  • face – The face name, for the error message.

  • what – The source element, as a path or a short description.

Returns:

value unchanged.

Raises:

FaceError – When value is None.

pain001.faces.common.transactions(pmt_inf: dict[str, Any], path: str) → list[tuple[dict[str, Any], str]][source]#

Each CdtTrfTxInf of a payment information block with its path.

pain001.faces.engine module#

Engine for face projections from pain.001 to external specifications (ADR-0005).

The engine builds the ISO JSON twin, runs the face’s projector and writes the loss report from indexed leaf paths: every occurrence of a repeated element is its own entry, so a dropped transaction can never pass for a mapped one. Faces are outbound only.

pain001.faces.engine.MAX_INLINE_BYTES = 10485760#

Largest document project_face_text() accepts, in UTF-8 bytes.

The same 10 MiB the REST dashboard accepts for an upload.

pain001.faces.engine.available_faces() → list[str][source]#

Return a sorted list of registered face names.

pain001.faces.engine.detect_version(xml: str) → str[source]#

Detect the pain.001 edition from XML namespace declarations.

Parameters:

xml – The XML text of the document.

Returns:

The detected version string, e.g. pain.001.001.09.

Raises:

FaceError – If no supported pain.001 namespace is detected.

pain001.faces.engine.list_supported_faces() → list[dict[str, Any]][source]#

Return each face with its description and accepted binding keys.

pain001.faces.engine.project_face(source: str | Path, face: str = 'uk_open_banking', *, version: str | None = None, bindings: dict[str, Any] | None = None) → FaceResult[source]#

Project a pain.001 document into an external industry standard face.

Parameters:
  • source – XML string or Path to an XML file.

  • face – The name of the target standard (default: uk_open_banking).

  • version – Optional explicit pain.001 edition (detected if omitted).

  • bindings – Optional private side inputs the face accepts (for example ConsentId for UK Open Banking). Keep them out of version control: they are never part of the source message.

Returns:

A FaceResult with payload and explicit LossReport.

Raises:

FaceError – If the face is unknown, a binding is not accepted, the twin cannot be built or the face refuses the document.

pain001.faces.engine.project_face_text(xml: str, face: str = 'uk_open_banking', *, version: str | None = None, bindings: dict[str, Any] | None = None) → FaceResult[source]#

Project a document a client sent as text, never read from a path.

project_face() reads a string that names an existing file, a convenience for the library and the command line. A surface that takes the document from someone else (the REST API, the MCP server, the language server) calls this instead: the text must be XML, at most MAX_INLINE_BYTES, with no entity declarations or external references, and the bindings must be a JSON object.

Parameters:
  • xml – The pain.001 document as XML text.

  • face – The name of the target standard.

  • version – Optional explicit pain.001 edition (detected if omitted).

  • bindings – Optional private side inputs the face accepts.

Returns:

A FaceResult, as project_face() returns it.

Raises:

FaceError – If the text is not XML, is too large, declares entities or external references, the bindings are not an object, or project_face() refuses the document.

pain001.faces.engine.register_face(name: str, func: Callable[[dict[str, Any], dict[str, Any] | None], tuple[dict[str, Any], set[str]]], description: str, bindings: tuple[str, ...] = ()) → None[source]#

Register a new projection face.

Parameters:
  • name – Unique name for the face.

  • func – Projection function returning (payload, mapped_paths), where the paths are indexed leaf paths relative to Document (see pain001.faces.paths.leaf_paths()).

  • description – Description of the target standard.

  • bindings – Binding keys the face accepts; any other is refused.

pain001.faces.iso_2018 module#

ISO 2018 compatibility face: full element names in snake_case (ADR-0005).

Each XML tag becomes its ISO 20022 message element name, read from iso_names.json, a table derived from the Registration Authority’s Message Definition Reports (Part 2) with the reports and the read date recorded (see scripts/build_iso_name_table.py), then written in snake_case: GrpHdr -> group_header. The message content is projected without the Document root, repeatable elements stay arrays, and an amount becomes {"amount", "currency"}. A tag the table lists as unresolved has no name to give, so that element and everything under it stay out of the payload and appear in the loss report.

pain001.faces.iso_2018.name_table() → dict[str, Any][source]#

The shipped tag-to-name table with its provenance.

pain001.faces.iso_2018.project_iso_2018(twin: dict[str, Any], bindings: dict[str, Any] | None = None) → tuple[dict[str, Any], set[str]][source]#

Project a pain.001 twin into ISO element names in snake_case.

Parameters:
  • twin – The ISO JSON twin of the document.

  • bindings – Unused: this face takes no side inputs (the engine refuses any).

Returns:

The projected message content and the set of source leaf paths it carries.

Raises:

FaceError – When the twin holds no pain.001 message.

pain001.faces.iso_2018.snake_case(name: str) → str[source]#

CreditTransferTransactionInformation -> credit_transfer_....

Runs of capitals stay one word: BICFI -> bicfi, AnyBIC -> any_bic, UETR -> uetr.

pain001.faces.models module#

Data models and results for face projections (ADR-0005).

exception pain001.faces.models.FaceError[source]#

Bases: ValueError

Raised when a face projection fails or an unsupported face is requested.

class pain001.faces.models.FaceResult(face: str, version: str, payload: dict[str, Any], loss_report: LossReport)[source]#

Bases: object

The result of projecting a pain.001 document into a target face.

Variables:
  • face (str) – The name of the target standard (e.g. uk_open_banking).

  • version (str) – The pain.001 source edition (e.g. pain.001.001.09).

  • payload (dict[str, Any]) – The JSON-serializable dictionary conforming to the target standard.

  • loss_report (pain001.faces.models.LossReport) – Details of which elements were mapped vs omitted.

face: str#
loss_report: LossReport#
payload: dict[str, Any]#
to_dict() → dict[str, Any][source]#

Return a complete dictionary containing payload and loss metadata.

to_json(indent: int = 2) → str[source]#

Serialize the projection payload to formatted JSON.

version: str#
class pain001.faces.models.LossReport(face: str, total_source_elements: int, mapped_paths: tuple[str, ...], unmapped_paths: tuple[str, ...])[source]#

Bases: object

Detailed report of mapped vs unmapped element paths in a projection.

Variables:
  • face (str) – The name of the projection standard.

  • total_source_elements (int) – Total number of leaf element paths in the source.

  • mapped_paths (tuple[str, ...]) – Element paths whose values were carried into the projection.

  • unmapped_paths (tuple[str, ...]) – Element paths present in the source that have no counterpart.

face: str#
property is_lossless: bool#

True when every source element path was carried into the target projection.

property loss_ratio: float#

Proportion of source elements not mapped into the target projection (0.0 to 1.0).

mapped_paths: tuple[str, ...]#
to_dict() → dict[str, Any][source]#

Convert the loss report into a JSON-serializable dictionary.

total_source_elements: int#
unmapped_paths: tuple[str, ...]#

pain001.faces.paths module#

Indexed leaf paths of an ISO JSON twin, and a reader that records them.

A loss report is only honest if every occurrence of a repeated element has its own path: PmtInf[0]/CdtTrfTxInf[1]/Amt/InstdAmt is a different leaf from PmtInf[0]/CdtTrfTxInf[0]/Amt/InstdAmt. Paths are relative to Document and carry an index on every array item. An amount object ({"amt", "Ccy"}) is one leaf plus its /@Ccy.

class pain001.faces.paths.Reader[source]#

Bases: object

Read values from a twin and record each leaf path that was used.

amount(node: Any, path: str) → tuple[str, str] | None[source]#

The (amount, currency) of an amount object, recorded.

mark() → frozenset[str][source]#

A snapshot of the recorded leaves, for rollback().

rollback(mark: frozenset[str]) → None[source]#

Forget leaves recorded since mark: their value was dropped.

text(node: Any, path: str, *keys: str) → str | None[source]#

The text leaf at keys below node, recorded, or None.

Parameters:
  • node – The twin node to start from.

  • path – The indexed path of node.

  • *keys – Element tags to descend through; each must be a single (non-repeatable) element.

Returns:

The leaf’s text, or None when any step is missing or the target is not a text leaf.

texts(node: Any, path: str, key: str) → list[str][source]#

Every text item of the repeatable element key, recorded.

pain001.faces.paths.is_amount(node: Any) → bool[source]#

True when node is a twin amount object {"amt", "Ccy"}.

pain001.faces.paths.join(path: str, key: str) → str[source]#

Append key to path with a slash, or start a path.

pain001.faces.paths.leaf_paths(twin: dict[str, Any]) → set[str][source]#

Every leaf path in the twin’s message, indexed per array item.

pain001.faces.uk_open_banking module#

UK Open Banking domestic payment face (ADR-0005).

Target: the request body of POST /domestic-payment-consents, OBWriteDomesticConsent4, in the Open Banking Read/Write API v4.0.1 (Open Banking Limited, MIT Open Licence). When the private bindings supply a ConsentId the same initiation becomes the OBWriteDomestic2 body of POST /domestic-payments.

A domestic payment carries one payment, so the face takes a document with exactly one transaction. Scheme names come from the Open Banking internal code set: UK.OBIE.IBAN for accounts, UK.OBIE.BICFI for the creditor agent. Elements without a counterpart (local instrument, purpose, charge bearer, execution date, regulatory reporting, …) are left to the loss report rather than translated by guesswork.

pain001.faces.uk_open_banking.project_uk_open_banking(twin: dict[str, Any], bindings: dict[str, Any] | None = None) → tuple[dict[str, Any], set[str]][source]#

Project a one-transaction pain.001 twin into a UK Open Banking body.

Parameters:
  • twin – The ISO JSON twin of the document.

  • bindings – Private side inputs: ConsentId (makes the body an OBWriteDomestic2) and Risk (the OBRisk1 object, which has no pain.001 source).

Returns:

The request body and the set of source leaf paths it carries.

Raises:

FaceError – When the document has more than one transaction, or lacks an element the target requires.