# ex_iso20022

[![Hex.pm](https://img.shields.io/hexpm/v/ex_iso20022.svg)](https://hex.pm/packages/ex_iso20022)
[![Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/ex_iso20022)
[![CI](https://github.com/ARTARNA/ex_iso20022/actions/workflows/ci.yml/badge.svg)](https://github.com/ARTARNA/ex_iso20022/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

ISO 20022 message parsing for Elixir. Currently covers **camt.053** (Bank to Customer
Statement), the highest-demand message type. More message types are planned.

## Installation

```elixir
# mix.exs
def deps do
  [
    {:ex_iso20022, "~> 0.1"}
  ]
end
```

## Quick start

```elixir
xml = File.read!("statement.xml")

case ISO20022.Camt053.parse(xml) do
  {:ok, doc} ->
    Enum.each(doc.statements, fn stmt ->
      IO.puts("Account IBAN : #{stmt.account.iban}")
      IO.puts("Currency     : #{stmt.account.currency}")

      closing = Enum.find(stmt.balances, &(&1.type == :closing_booked))
      IO.puts("Closing bal  : #{closing.amount} #{closing.currency} (#{closing.credit_debit})")

      Enum.each(stmt.entries, fn entry ->
        IO.puts("  #{entry.ref}  #{entry.credit_debit}  #{entry.amount} #{entry.currency}")
      end)
    end)

  {:error, reason} ->
    IO.inspect(reason, label: "parse error")
end
```

Using the bang variant when you are confident the input is valid:

```elixir
doc = ISO20022.Camt053.parse!(xml)
```

## Supported message types

| Module | Message | Versions |
|--------|---------|----------|
| `ISO20022.Camt053` | Bank to Customer Statement | camt.053.001.02 – 014 |

More message types (camt.052, camt.054, pain.001, pacs.008, …) will be added in
subsequent releases. The top-level `ISO20022.parse/1` dispatcher is already in place
and will route to the right module automatically once each type is implemented.

## Struct reference

### `ISO20022.Camt053.Document`

The top-level struct returned by `parse/1`.

| Field | Type | Description |
|-------|------|-------------|
| `group_header` | `GroupHeader` | Message-level metadata |
| `statements` | `[Statement]` | One entry per account per period |

### `ISO20022.Camt053.GroupHeader`

| Field | Type | Description |
|-------|------|-------------|
| `message_id` | `String` | Unique message identifier (max 35 chars) |
| `created_at` | `DateTime` | UTC creation timestamp |
| `pagination` | `map \| nil` | `%{page_number: String, last_page: boolean}` |

### `ISO20022.Camt053.Statement`

| Field | Type | Description |
|-------|------|-------------|
| `id` | `String` | Statement identifier |
| `electronic_seq_number` | `integer \| nil` | Sequence number for gap detection |
| `created_at` | `DateTime \| nil` | Statement generation time |
| `from_to_date` | `map \| nil` | `%{from: DateTime, to: DateTime}` |
| `account` | `Account` | Account identification |
| `balances` | `[Balance]` | At least opening and closing booked balances |
| `entries` | `[Entry]` | Booked transactions |

### `ISO20022.Camt053.Account`

| Field | Type | Notes |
|-------|------|-------|
| `iban` | `String \| nil` | Present when account is IBAN-identified |
| `other_id` | `String \| nil` | Present when account uses a non-IBAN scheme |
| `other_scheme` | `String \| nil` | Scheme code, e.g. `"BBAN"` |
| `currency` | `String \| nil` | ISO 4217 alpha code, e.g. `"EUR"` |
| `servicer_bic` | `String \| nil` | BIC of the account-servicing institution |
| `name` | `String \| nil` | Account name |

### `ISO20022.Camt053.Balance`

`amount` is always a positive `Decimal`. The sign is expressed through `credit_debit`.

| Field | Type | Description |
|-------|------|-------------|
| `type` | atom | See balance types below |
| `amount` | `Decimal` | Positive amount |
| `currency` | `String` | ISO 4217 alpha code |
| `credit_debit` | `:credit \| :debit` | `:debit` means overdraft |
| `date` | `Date` | Balance reference date |

Balance type atoms:

| Atom | ISO code | Meaning |
|------|----------|---------|
| `:opening_booked` | `OPBD` | Opening booked (mandatory) |
| `:closing_booked` | `CLBD` | Closing booked (mandatory) |
| `:closing_available` | `CLAV` | Closing available |
| `:interim_booked` | `ITBD` | Interim booked |
| `:interim_available` | `ITAV` | Interim available |
| `:forward_available` | `FWAV` | Forward available |
| `{:other, code}` | any | Unrecognised code |

### `ISO20022.Camt053.Entry`

| Field | Type | Description |
|-------|------|-------------|
| `ref` | `String` | Bank-assigned entry reference |
| `amount` | `Decimal` | Positive amount |
| `currency` | `String` | ISO 4217 alpha code |
| `credit_debit` | `:credit \| :debit` | Direction |
| `reversal` | `boolean` | `true` if this cancels a prior entry |
| `status` | `:booked` | Always `:booked` in camt.053 |
| `booking_date` | `Date \| nil` | Date posted to account |
| `value_date` | `Date \| nil` | Value date (may differ from booking date) |
| `account_servicer_ref` | `String \| nil` | Bank's own reference |
| `bank_transaction_code` | `BankTxCode \| nil` | ISO 20022 transaction classification |
| `additional_info` | `String \| nil` | Free-text entry description |
| `details` | `[EntryDetails]` | Transaction-level detail blocks (batch entries) |

### `ISO20022.Camt053.BankTxCode`

| Field | Type | Example |
|-------|------|---------|
| `domain` | `String \| nil` | `"PMNT"` |
| `family` | `String \| nil` | `"RCDT"`, `"ICDT"`, `"RDDT"` |
| `sub_family` | `String \| nil` | `"XBCT"`, `"ESCT"`, `"SALA"` |
| `proprietary_code` | `String \| nil` | Bank-specific code |
| `proprietary_issuer` | `String \| nil` | Issuer of the proprietary code |

### `ISO20022.Camt053.EntryDetails`

Present when an entry groups multiple underlying transactions (batch payments).

| Field | Type | Description |
|-------|------|-------------|
| `batch` | `map \| nil` | `%{message_id, payment_info_id, number_of_transactions, total_amount}` |
| `transaction_details` | `[TransactionDetails]` | Individual transaction records |

### `ISO20022.Camt053.TransactionDetails`

| Field | Type | Description |
|-------|------|-------------|
| `refs` | `map \| nil` | `%{message_id, end_to_end_id, uetr, …}` |
| `amount` | `Decimal \| nil` | Individual transaction amount |
| `currency` | `String \| nil` | ISO 4217 |
| `credit_debit` | `:credit \| :debit \| nil` | Direction |
| `related_parties` | `map \| nil` | `%{debtor, creditor, ultimate_debtor, ultimate_creditor}` |
| `related_agents` | `map \| nil` | `%{debtor_agent, creditor_agent}` |
| `remittance_info` | tuple / nil | `{:unstructured, text}` or `{:structured, %{ref, ref_type, …}}` |
| `purpose` | `String \| nil` | ISO 20022 purpose code |

## Error handling

```elixir
{:ok, %ISO20022.Camt053.Document{}}

# Malformed XML
{:error, {:parse_error, reason}}

# Namespace not recognised as a camt.053 variant
{:error, {:unsupported_version, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09"}}

# Mandatory field absent from an otherwise valid document
{:error, {:missing_required_field, [:group_header, :message_id]}}
{:error, {:missing_required_field, [:statements, 0, :id]}}

# Field present but value could not be parsed
{:error, {:invalid_amount, "N/A", [:statements, 0, :entries, 1, :amount]}}
{:error, {:invalid_date, "32-01-2024"}}
```

The path in `missing_required_field` and `invalid_*` errors follows the struct
hierarchy so it is straightforward to pinpoint the problematic node.

## Multi-version support

Real-world bank files use a wide range of schema versions:

```
urn:iso:std:iso:20022:tech:xsd:camt.053.001.02   # many European banks
urn:iso:std:iso:20022:tech:xsd:camt.053.001.04   # UK, SEPA migration era
urn:iso:std:iso:20022:tech:xsd:camt.053.001.08   # current SWIFT / TARGET2
urn:iso:std:iso:20022:tech:xsd:camt.053.001.11   # newer implementations
urn:iso:std:iso:20022:tech:xsd:camt.053.001.14   # latest ISO (2026)
```

`ex_iso20022` detects the version from the root element's `xmlns` attribute and
normalises to the same struct regardless of input version. All versions 02 – 14 are
accepted. Documents lacking a namespace (some older senders) are also accepted.

## License

MIT
