Ex_Iso8583

An Elixir library for parsing and formatting ISO 8583 messages - the international standard for systems that exchange electronic transaction information.

Architecture

Module Overview

Ex_Iso8583
    |
    +-- IsoBitmap            - Bitmap management (creation, parsing, transformation)
    +-- IsoField             - Field extraction and formatting
    +-- IsoFieldFormat       - Field format definition parsing
    +-- IsoMsg               - Message structure definition with helper functions
    +-- Util                 - Utility functions for data manipulation
    |
    +-- TransactionType      - Type-safe transaction struct definitions
    +-- TransactionTypeGroup - Transaction grouping (request/response pairs)
    |
    +-- TransactionProcessor - Pure functional transaction processing DSL
         +-- Middleware       - Logging, timing, validation, transformation
         +-- TimeoutWrapper   - Timeout handling with automatic timeout response
    |
    +-- Iso8583.Formatter    - Behaviour for wire format encoding/decoding
    +-- Iso8583.Client       - High-level client with automatic encoding/decoding
    +-- Iso8583.Formatters   - Built-in formatters (Binary, AsciiHex)
    |
    +-- Iso8583.Connectivity - Transport abstraction layer
         +-- Context          - Struct for transport metadata
         +-- Transport        - Behaviour for transport implementations
         +-- Handler          - Generic handler connecting processor + transport
         +-- Transport.TCP    - TCP Server and Client implementations
         +-- Transport.HTTP   - HTTP Server implementation (JSON/REST API)
         +-- Transport.WebSocket - WebSocket Server and Client implementations
         +-- Transport.UDP    - UDP Server and Client implementations (planned)

Core Modules

Ex_Iso8583 - Main API

The primary entry point for ISO 8583 message operations:

IsoBitmap - Bitmap Management

Handles the bitmap that indicates which data elements are present in a message:

Key functions:

The bitmap follows ISO 8583 standards:

IsoField - Field Operations

Handles individual field formatting and extraction:

Supported Data Types: | Type | Description | |------|-------------| | :bcd | Binary Coded Decimal - numeric data packed 2 digits per byte | | :ascii | ASCII text representation | | :z | Track 2 data (special BCD encoding) | | :binary | Raw binary data | | :hex | Hexadecimal data |

Key functions:

IsoFieldFormat - Format Definition Parser

Parses field format definitions like "n ..19" or "an ...12":

Format Syntax:

Length Indicators:

IsoMsg - Message Structure

Defines a struct for ISO message representation:

defstruct config: %{ascii_format: false, ascii_bitmap: true, tpdu_length: 10},
          tpdu: "",
          mti: "",
          data: %{}

Util - Utility Functions

Common helper functions:

Transaction Types

TransactionType - Type-Safe Transaction Definitions

Define strongly-typed transaction structs with automatic encoding/decoding:

defmodule SaleRequest do
  use Ex_Iso8583.TransactionType

  transaction_type "0200", "001000"

  defstruct [:pan, :amount, :stan, :terminal_id, :processing_code]

  field_mapping %{
    pan: 2,
    amount: 4,
    stan: 11,
    terminal_id: 41,
    processing_code: 3
  }

  field_formats %{
    2 => "n ..19",
    3 => "n 6",
    4 => "n 12",
    11 => "n 6",
    41 => "ans ..8"
  }

  mandatory_fields [:pan, :amount, :stan, :processing_code]
end

Key features:

TransactionTypeGroup - Transaction Grouping

Group related transaction types (request/response pairs):

defmodule SaleTransaction do
  use Ex_Iso8583.TransactionTypeGroup

  request SaleRequest
  response SaleResponse

  response_mti "0210"
end

Transaction Processing

TransactionProcessor - Pure Functional Handler DSL

The TransactionProcessor provides a macro-based DSL for defining transaction handlers with hooks and middleware. It follows a pure functional approach with no processes or supervision.

Processing Flow:

┌─────────────────────────────────────────────────────────────────────┐
│                         TransactionProcessor                         │
│                                                                     │
│  1. PARSE        Raw ISO Message ──► Request Struct                 │
│  2. FIND         Match Request Module ──► Handler                    │
│  3. VALIDATE     Check Mandatory Fields                              │
│  4. BEFORE HOOKS Execute validation/transform (can raise errors)     │
│  5. HANDLE       Execute business logic ──► Response Struct          │
│  6. AFTER HOOKS  Execute logging/post-processing                      │
│  7. RETURN       {:ok, Response} | {:error, Reason}                  │
└─────────────────────────────────────────────────────────────────────┘

Define a processor:

defmodule MyProcessor do
  use TransactionProcessor

  config error_response_code_field: 39,
        error_message_field: 60

  # Sale handler with validation and logging hooks
  defhandler :sale, SaleRequest, SaleResponse,
    before_hooks: [:validate_amount],
    after_hooks: [:log_response] do

    def handle(%SaleRequest{amount: amount, stan: stan}) do
      # Business logic
      %SaleResponse{
        response_code: "00",
        amount: amount,
        stan: stan,
        auth_code: generate_auth_code()
      }
    end

    # Hooks must be public (def, not defp)
    def validate_amount(%SaleRequest{amount: amt} = req) when amt > 0, do: req
    def validate_amount(_), do: raise(ArgumentError, "Invalid amount")

    def log_response(resp), do: resp

    defp generate_auth_code, do: :rand.uniform(999_999) |> to_string()
  end

  # Void handler
  defhandler :void, VoidRequest, VoidResponse do
    def handle(%VoidRequest{stan: stan}) do
      %VoidResponse{response_code: "00", stan: stan}
    end
  end
end

Use the processor:

# Process raw ISO message
{:ok, response} = MyProcessor.process(raw_iso_message)

# Process pre-parsed struct
request = %SaleRequest{amount: 10000, stan: "000123", pan: "...", terminal_id: "TERM001"}
{:ok, response} = MyProcessor.process_struct(request)

Middleware:

defmodule MyProcessor do
  use TransactionProcessor

  # Built-in middleware
  use_middleware TransactionProcessor.Middleware.Logger
  use_middleware TransactionProcessor.Middleware.Timer

  # Or custom middleware
  defmodule AuthMiddleware do
    @behaviour TransactionProcessor.Middleware

    def call(request, next) do
      if authenticated?(request) do
        next.(request)
      else
        {:error, :unauthorized}
      end
    end
  end
end

Key features:

TransactionProcessor.TimeoutWrapper - Timeout Handling

The TimeoutWrapper adds timeout capability to TransactionProcessor while keeping the core processor pure functional. It handles the side effects (timing) in a separate layer.

Why use TimeoutWrapper?

Configuration:

defmodule PaymentProcessor do
  use TransactionProcessor.TimeoutWrapper,
    processor: MyProcessor,                    # Required: processor to wrap
    timeouts: %{                                # Required: per-type timeouts (ms)
      # Fast transactions
      sale: 5000,                              # 5 seconds
      void: 3000,                              # 3 seconds
      refund: 3000,                            # 3 seconds
      balance_inquiry: 2000,                   # 2 seconds

      # Slower transactions
      sale_with_cashback: 7000,                # 7 seconds
      capture: 5000,                           # 5 seconds
      reversal: 4000,                          # 4 seconds

      # Batch operations (much slower)
      batch_close: 60000,                      # 60 seconds
      settlement: 120000,                      # 2 minutes

      # Default for unknown types
      default: 5000
    },
    timeout_response_field: 39,                 # Optional: field for timeout code (default: 39)
    timeout_response_code: "68"                 # Optional: timeout value (default: "68" per ISO 8583)
end

# Process raw ISO message with timeout
{:ok, response} = PaymentProcessor.process_with_timeout(raw_iso_message)

# Process pre-parsed struct with timeout
{:ok, response} = PaymentProcessor.process_struct_with_timeout(request_struct)

Transaction Type Detection:

Transaction types are automatically detected from MTI and Processing Code:

MTI Processing Code Transaction Type
0200 001000 sale
0200 002000 sale_with_cashback
0200 00xxxx balance_inquiry
0200 310000 batch_close
0220 001000 refund
0400 001000 capture
0420 001000 capture_refund
0400 002000 void
0420 002000 reversal
0500 001000 settlement
0800 001000 network_management

Use a custom detector for non-standard mappings:

defmodule CustomProcessor do
  use TransactionProcessor.TimeoutWrapper,
    processor: MyProcessor,
    timeouts: %{custom_type: 5000},
    transaction_type_detector: &MyModule.detect_transaction_type/1
end

Handling Timeout Responses:

case PaymentProcessor.process_with_timeout(raw_message) do
  {:ok, response} ->
    case Map.get(response, 39) do
      "68" ->
        # Transaction timed out
        Logger.warning("Transaction timed out", stan: Map.get(response, 11))
        # Response still has STAN and other copied fields for reconciliation

      "00" ->
        # Transaction approved
        Logger.info("Transaction approved")

      other ->
        # Other response code
        Logger.warning("Transaction declined: #{other}")
    end

  {:error, reason} ->
    Logger.error("Processing error: #{inspect(reason)}")
end

Adding to Supervision Tree:

The TimeoutWrapper requires a Task.Supervisor. Add it to your application's children:

# In your application.ex
defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      TransactionProcessor.TimeoutSupervisor,
      # ... other children
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Timeout Response Details:

When a timeout occurs:

Timeout Value Guidelines:

Transaction Type Recommended Timeout Reason
balance_inquiry 2000ms Quick database lookup
sale/void/refund 5000ms External API calls
capture 5000ms Acquiring funds
reversal 4000ms Fast processing needed
batch_close 60000ms Multiple operations
settlement 120000ms Batch processing

Features:

Connectivity Layer

The connectivity layer provides transport abstraction - decoupling how ISO 8583 messages are transferred from your business logic.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                     Your Application                         │
└─────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┴───────────────┐
              │                               │
      ┌───────────────┐               ┌───────────────┐
      │  TCP Server   │               │  HTTP Server  │
      │  (Terminals)  │               │  (REST API)   │
      └───────────────┘               └───────────────┘
              │                               │
              └───────────────┬───────────────┘
                              │
              ┌───────────────────────────────────────┐
              │        Iso8583.Handler                │
              │  - Pluggable Transport               │
              │  - Your TransactionProcessor         │
              └───────────────────────────────────────┘
                              │
              ┌───────────────────────────────────────┐
              │    TransactionProcessor               │
              │    (Your business logic)              │
              └───────────────────────────────────────┘

Iso8583.Handler

The Iso8583.Handler module provides a use macro that creates a GenServer handler connecting your processor with any transport:

defmodule MyApp.PaymentHandler do
  use Iso8583.Handler,
    processor: MyApp.PaymentProcessor,
    transport: Iso8583.Transport.TCP.Server,
    transport_opts: [
      port: 8080,
      acceptors: 10
    ]
end

Add to your supervision tree:

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      # TCP Server - accepts connections from terminals
      MyApp.PaymentHandler,

      # HTTP Server - for REST API
      {Iso8583.Handler,
       processor: MyApp.PaymentProcessor,
       transport: Iso8583.Transport.HTTP.Server,
       transport_opts: [port: 4000]},

      # TCP Client - connects to upstream acquirer
      {Iso8583.Handler,
       processor: MyApp.UpstreamProcessor,
       transport: Iso8583.Transport.TCP.Client,
       transport_opts: [
         host: "acquirer.example.com",
         port: 9000
       ]}
    ]

    opts = [strategy: :one_for_one]
    Supervisor.start_link(children, opts)
  end
end

Available Transports

Transport Type Status Description
Iso8583.Transport.TCP.Server Server ✅ Implemented Accept TCP connections from clients
Iso8583.Transport.TCP.Client Client ✅ Implemented Connect to TCP server
Iso8583.Transport.HTTP.Server Server ✅ Implemented HTTP/HTTPS server with JSON API
Iso8583.Transport.HTTP.Client Client Planned HTTP client for upstream calls
Iso8583.Transport.UDP.Server Server Planned Receive UDP datagrams
Iso8583.Transport.UDP.Client Client Planned Send UDP datagrams

TCP Server Transport

Accepts TCP connections and receives ISO 8583 messages:

defmodule MyApp.TerminalHandler do
  use Iso8583.Handler,
    processor: MyApp.PaymentProcessor,
    transport: Iso8583.Transport.TCP.Server,
    transport_opts: [
      port: 8080,           # Port to listen on
      acceptors: 10,         # Number of acceptor processes
      packet_handler: :raw,  # How to parse messages (:raw, :line, {:size, bytes})
      timeout: 60000         # Connection idle timeout (ms)
    ]
end

Packet Handlers:

TCP Client Transport

Connects to a remote TCP server:

defmodule MyApp.UpstreamHandler do
  use Iso8583.Handler,
    processor: MyApp.UpstreamProcessor,
    transport: Iso8583.Transport.TCP.Client,
    transport_opts: [
      host: "acquirer.example.com",
      port: 9000,
      reconnect_interval: 5000,  # Reconnect delay on disconnect (ms)
      timeout: 60000
    ]
end

HTTP Server Transport

Provides a REST API for sending ISO 8583 messages over HTTP.

Request Format:

POST /iso8583
Content-Type: application/json

{
  "iso_message": "base64_encoded_iso8583_binary",
  "request_id": "optional-correlation-id"
}

Response Format (Success):

{
  "iso_message": "base64_encoded_response",
  "request_id": "same-as-request"
}

Response Format (Error):

{
  "error": "error_message",
  "request_id": "same-as-request"
}
defmodule MyApp.ApiHandler do
  use Iso8583.Handler,
    processor: MyApp.PaymentProcessor,
    transport: Iso8583.Transport.HTTP.Server,
    transport_opts: [
      port: 4000,           # HTTP port
      path: "/iso8583",     # API endpoint path
      scheme: :http,        # :http or :https
      timeout: 30000,       # Request timeout (ms)
      cors_origins: ["https://example.com"]  # Optional CORS
    ]
end

HTTPS Support:

transport_opts: [
  port: 8443,
  scheme: :https,
  certfile: "/path/to/cert.pem",
  keyfile: "/path/to/key.pem"
]

HTTP Context Metadata:

Iso8583.Context

The context carries transport-specific metadata alongside messages:

# Fields in context
%Iso8583.Context{
  transport_ref: socket,        # Transport-specific reference
  client_id: "client_123",      # Optional client identifier
  peer_address: {192, 168, 1, 100},  # Client's IP address
  request_id: "req-abc123",     # Correlation ID for tracing
  transport_metadata: %{        # Transport-specific data
    connection_time: 1234567890,
    bytes_received: 1024
  }
}

Custom Transport

Implement your own transport by using the Iso8583.Transport behaviour:

defmodule MyCustomTransport do
  @behaviour Iso8583.Transport

  def start_link(opts) do
    # Start your transport
  end

  def send(transport_ref, data) do
    # Send data
  end

  def set_receive_callback(pid, callback) do
    # Register callback for incoming messages
  end

  def stop(pid) do
    # Stop transport
  end
end

Formatter and Client API

The library provides a high-level API for working with transaction structs instead of raw binaries. This is useful when building proxy/gateway applications that need to transform messages between different wire formats.

Iso8583.Formatter Behaviour

The Iso8583.Formatter behaviour defines how to convert between ISOMsg structs and wire format binaries. Different backends may use different encodings (binary, ASCII hex, etc.).

defmodule MyApp.CustomFormatter do
  @behaviour Iso8583.Formatter

  @impl true
  def encode(%ISOMsg{} = iso_msg) do
    # Convert ISOMsg to your wire format
    mti = ISOMsg.get_mti(iso_msg)
    data = ISOMsg.get_data(iso_msg)
    # ... encoding logic
    mti <> bitmap <> fields_data
  end

  @impl true
  def decode(raw_binary) do
    # Convert wire format to ISOMsg
    # ... parsing logic
    {:ok, %ISOMsg{mti: mti, data: data}}
  end
end

Built-in Formatters

Iso8583.Formatters.Binary

Standard binary ISO 8583 format:

# Encode
iso_msg = ISOMsg.new("0200", %{2 => "1234567890123456789", 4 => "000000001234"})
raw_binary = Iso8583.Formatters.Binary.encode(iso_msg)

# Decode
{:ok, iso_msg} = Iso8583.Formatters.Binary.decode(raw_binary)

Iso8583.Formatters.AsciiHex

ASCII hex format commonly used by legacy systems:

# Encode
iso_msg = ISOMsg.new("0200", %{2 => "1234567890123456789", 4 => "000000001234"})
raw_binary = Iso8583.Formatters.AsciiHex.encode(iso_msg)

# Decode
{:ok, iso_msg} = Iso8583.Formatters.AsciiHex.decode(raw_binary)

ISOMsg Helper Functions

The ISOMsg module provides helper functions for working with ISO messages:

# Create new ISOMsg
iso_msg = ISOMsg.new("0200", %{2 => "1234567890123456789", 4 => "000000001234"})

# Get/set MTI
mti = ISOMsg.get_mti(iso_msg)  # => "0200"
iso_msg = ISOMsg.set_mti(iso_msg, "0210")

# Get/set fields
pan = ISOMsg.get_field(iso_msg, 2)  # => "1234567890123456789"
iso_msg = ISOMsg.set_field(iso_msg, 39, "00")  # Set response code

# Check for fields
ISOMsg.has_field?(iso_msg, 2)  # => true
ISOMsg.fields(iso_msg)  # => [2, 4]

# Convert struct to/from ISOMsg
defmodule SaleRequest do
  defstruct [:pan, :amount, :stan]

  def __iso_field_map__, do: %{2 => :pan, 4 => :amount, 11 => :stan}
  def __iso_mti__, do: "0200"
end

request = %SaleRequest{pan: "123456...", amount: "1000", stan: "000001"}

# Struct -> ISOMsg
iso_msg = ISOMsg.from_struct(request, "0200", %{2 => :pan, 4 => :amount, 11 => :stan})

# ISOMsg -> Struct
request = ISOMsg.to_struct(iso_msg, SaleRequest, %{2 => :pan, 4 => :amount, 11 => :stan})

Iso8583.Client - High-Level Transaction API

The Iso8583.Client module provides a simplified API for sending transactions with automatic encoding/decoding and response correlation.

Define Your Transaction Struct

defmodule MyApp.SaleRequest do
  defstruct [:pan, :amount, :stan, :terminal_id]

  # Formatter to use for encoding/decoding
  def __iso_formatter__, do: Iso8583.Formatters.Binary

  # Map ISO field numbers to struct fields
  def __iso_field_map__, do: %{
    2 => :pan,
    4 => :amount,
    11 => :stan,
    41 => :terminal_id
  }

  # Default MTI for this transaction type
  def __iso_mti__, do: "0200"
end

defmodule MyApp.SaleResponse do
  defstruct [:response_code, :amount, :stan, :auth_code]

  def __iso_formatter__, do: Iso8583.Formatters.Binary
  def __iso_field_map__, do: %{
    39 => :response_code,
    4 => :amount,
    11 => :stan,
    38 => :auth_code
  }

  def __iso_response_module__, do: __MODULE__
end

Start the Client in Your Supervision Tree

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      # Backend client with Binary formatter
      {Iso8583.Client, name: :backend,
       transport: Iso8583.Transport.TCP.Client,
       transport_opts: [
         host: "backend.example.com",
         port: 9000,
         framing: {:length_prefix, 2}
       ],
       formatter: Iso8583.Formatters.Binary,
       request_timeout: 30000},

      # Terminal client with ASCII Hex formatter (different format!)
      {Iso8583.Client, name: :terminal_acquirer,
       transport: Iso8583.Transport.TCP.Client,
       transport_opts: [
         host: "acquirer.example.com",
         port: 9100,
         framing: {:length_prefix, 2}
       ],
       formatter: Iso8583.Formatters.AsciiHex,
       request_timeout: 30000}
    ]

    opts = [strategy: :one_for_one]
    Supervisor.start_link(children, opts)
  end
end

Send Transactions

# Create request
request = %SaleRequest{
  pan: "1234567890123456789",
  amount: "000000001234",
  stan: "000001",
  terminal_id: "12345678"
}

# Send to backend - automatically encodes using configured formatter
case Iso8583.Client.send_transaction(:backend, request) do
  {:ok, %SaleResponse{response_code: "00"} = response} ->
    Logger.info("Transaction approved", auth_code: response.auth_code)

  {:ok, %SaleResponse{response_code: code}} ->
    Logger.warning("Transaction declined: #{code}")

  {:error, reason} ->
    Logger.error("Transaction failed: #{inspect(reason)}")
end

Proxy/Gateway Pattern

The Formatter + Client combination is ideal for building proxy/gateway applications that translate between different wire formats:

defmodule MyApp.Gateway do
  use GenServer

  # Handle request from terminal (Binary format)
  def handle_terminal_request(raw_binary, context) do
    # Decode using terminal&#39;s formatter
    {:ok, iso_msg} = Iso8583.Formatters.Binary.decode(raw_binary)

    # Convert to struct
    request = ISOMsg.to_struct(iso_msg, SaleRequest, %{
      2 => :pan, 4 => :amount, 11 => :stan, 41 => :terminal_id
    })

    # Transform if needed
    request = %{request | amount: transform_amount(request.amount)}

    # Send to backend (uses configured formatter - could be AsciiHex!)
    Iso8583.Client.send_transaction(:backend, request)
  end

  # Handle response from backend (decode to struct)
  def handle_backend_response(%SaleResponse{} = response) do
    # Transform response if needed
    response = %{response | response_code: map_code(response.response_code)}

    # Send back to terminal
    iso_msg = ISOMsg.from_struct(response, "0210", %{
      39 => :response_code, 4 => :amount, 11 => :stan, 38 => :auth_code
    })

    raw_binary = Iso8583.Formatters.Binary.encode(iso_msg)
    # Send via transport...
  end

  defp transform_amount(amount), do: amount
  defp map_code("00"), do: "00"
  defp map_code(_), do: "05"
end

Key benefits:

Installation

def deps do
  [
    {:ex_iso8583, "~> 0.3.2"}
  ]
end

Usage

Message Type Configuration

Define your message type format:

# For BCD-packed fields
msg_type_bcd = %{
  bitmap_type: :binary,    # or :ascii for hex-encoded bitmap
  field_header_type: :bcd  # or :ascii
}

# For ASCII fields
msg_type_ascii = %{
  bitmap_type: :ascii,
  field_header_type: :ascii
}

Padding Configuration

Configure default padding behavior for fixed-length fields:

msg_type_with_padding = %{
  bitmap_type: :binary,
  field_header_type: :bcd,
  padding: %{
    bcd: %{char: "0", direction: :left},    # Default: pad with zeros on the left
    ascii: %{char: " ", direction: :left},  # Default: pad with spaces on the left
    z: %{char: "0", direction: :right}      # Track 2: pad with zeros on the right
  }
}

Per-Field Padding Override

Override padding for specific fields using a map format:

field_format = %{
  # Simple string format (uses default padding)
  2 => "n ..19",
  3 => "n 6",
  4 => "n 12",

  # Map format with custom padding
  48 => %{
    format: "ans ...999",
    padding: %{char: " ", direction: :right}  # Right-pad with spaces
  },

  # Disable padding for a specific field
  42 => %{
    format: "ans ...999",
    padding: false
  }
}

Padding Options:

Option Type Description
char String Character to use for padding (e.g., "0", " ")
direction Atom :left or :right
false Boolean Disable padding for this field

Note: Padding only applies to fixed-length fields (fields without a length header). Variable-length fields (with .. or ... in format) are not padded.

Field Format Definition

Define the format for each data element:

field_format = %{
  2 => "n ..19",   # Field 2: Numeric, variable up to 19 digits (2-byte length header)
  3 => "n 6",      # Field 3: Numeric, fixed 6 digits (no header)
  4 => "n 12",     # Field 4: Numeric, fixed 12 digits
  35 => "z ..37",  # Field 35: Track 2 data, variable up to 37
  52 => "b 64",    # Field 52: Binary, 8 bytes (64 bits)
  # ... more fields
}

Parsing a Message

# Raw ISO message (without MTI/TPDU)
raw_msg = <<0x22, 0x00, 0x02, 0x20, 0x00, 0x00, 0x04, 0x00, ...>>

# Parse into a map
fields = Ex_Iso8583.extract_iso_msg(raw_msg, msg_type_bcd, field_format)
# => %{2 => "1234567890123456789", 3 => "123456", 4 => "000000001234", ...}

Building a Message

# Define field data
data = %{
  2 => "1234567890123456789",
  3 => "123456",
  4 => "000000001234",
  35 => "1234567890123456789D231201234567890"
}

# Build ISO message binary
iso_msg = Ex_Iso8583.form_iso_msg(data, msg_type_bcd, field_format)
# => <<0x22, 0x00, 0x02, 0x20, ...>>

ISO 8583 Message Structure

+--------+--------+----------+------------------+
|  MTI   | Bitmap |  Fields  |   Field Data     |
| 4 bytes| 8/16   | (Variable) per bitmap    |
+--------+--------+----------+------------------+
  1. MTI (Message Type Indicator) - 4 digits defining the message class
  2. Bitmap - Indicates which fields are present
  3. Fields - Data elements as defined by the bitmap

Data Type Details

BCD (Binary Coded Decimal)

Track 2 (Type Z)

ASCII

Binary

Testing

Run the test suite:

mix test

The project uses property-based testing with stream_data for robust validation.

License

See LICENSE file for details.