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:
extract_iso_msg/3- Parse an ISO 8583 binary message into a map of fieldsform_iso_msg/3- Build an ISO 8583 binary message from a field map
IsoBitmap - Bitmap Management
Handles the bitmap that indicates which data elements are present in a message:
- Binary Bitmap - Raw binary format (8 or 16 bytes)
- ASCII Bitmap - Hex-encoded string representation (16 or 32 characters)
Key functions:
create_bitmap/1- Create bitmap from field mapbitmap_to_list/1- Convert bitmap to list of field numbers presentlist_to_bitmap/1- Convert field list to binary bitmapsplit_bitmap_and_msg/2- Separate bitmap from message data
The bitmap follows ISO 8583 standards:
- If bit 1 is set → Secondary bitmap exists (fields 65-128)
- If bit 1 is NOT set → Only primary bitmap (fields 2-64)
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:
form_field/3- Format a single field for outputextract_field/3- Extract a single field from input
IsoFieldFormat - Format Definition Parser
Parses field format definitions like "n ..19" or "an ...12":
Format Syntax:
n- Numerica- Alphabetican- Alphanumericans- Alphanumeric + Specialb- Binaryz- Track 2x+n- Variable length with header
Length Indicators:
n 6- Fixed length of 6n ..19- Variable length up to 19 (with 2-byte header)n ...104- Variable length up to 104 (with 3-byte header)
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:
- String padding (left/right with BCD/ASCII)
- Numeric sanitization
- BCD length calculation
- Binary-to-hex conversion
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]
endKey features:
- Automatic ISO message encoding/decoding
- Type-safe field mapping
- Mandatory field validation
- Support for copyable fields (request → response)
TransactionTypeGroup - Transaction Grouping
Group related transaction types (request/response pairs):
defmodule SaleTransaction do
use Ex_Iso8583.TransactionTypeGroup
request SaleRequest
response SaleResponse
response_mti "0210"
endTransaction 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
endUse 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
endKey features:
- Type-safe handlers - Compile-time validation for request/response types
- Before/after hooks - Validation, transformation, logging
- Middleware pipeline - Composable cross-cutting concerns
- Error handling - Automatic error response generation
- Pure functional - No processes, no supervision (handled separately)
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?
- Database delays - Protect against slow DB queries hanging transactions
- External service calls - Timeout when downstream services are unresponsive
- Resource management - Prevent resource exhaustion from hung transactions
- SLA compliance - Ensure transactions complete within time limits
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
endHandling 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)}")
endAdding 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
endTimeout Response Details:
When a timeout occurs:
- The processing task is terminated immediately
- A timeout response is generated with your configured code (default: "68")
- Common fields are copied from the request (STAN, terminal ID, etc.)
- The response struct matches your expected response module type
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:
- Per-transaction-type timeout configuration
- Automatic timeout response generation
- Task isolation for clean termination
- Transaction type detection from MTI + processing code
- No modification to TransactionProcessor required (wrapper pattern)
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
]
endAdd 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
endAvailable 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)
]
endPacket Handlers:
:raw- Read entire socket buffer (default):line- Read until newline{:size, bytes}- Read fixed-size messages
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
]
endHTTP 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
]
endHTTPS Support:
transport_opts: [
port: 8443,
scheme: :https,
certfile: "/path/to/cert.pem",
keyfile: "/path/to/key.pem"
]HTTP Context Metadata:
transport_ref- ThePlug.Connstructclient_id-"http_client"peer_address- Client's IP fromconn.remote_iptransport_metadata-%{method, path, headers, user_agent, content_type}
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
endFormatter 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
endBuilt-in Formatters
Iso8583.Formatters.Binary
Standard binary ISO 8583 format:
- Binary MTI (4 bytes)
- Binary bitmap (8 or 16 bytes)
- BCD/ASCII encoded fields
# 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:
- ASCII MTI (4 characters)
- ASCII hex bitmap (16 or 32 hex characters)
- ASCII encoded fields
# 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__
endStart 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
endSend 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)}")
endProxy/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'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"
endKey benefits:
- Work with structured data instead of raw binaries
- Automatic request/response correlation using STAN (field 11)
- Different formatters for different backends
- Clean separation of business logic from wire format
Installation
def deps do
[
{:ex_iso8583, "~> 0.3.2"}
]
endUsage
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 |
+--------+--------+----------+------------------+- MTI (Message Type Indicator) - 4 digits defining the message class
- Bitmap - Indicates which fields are present
- Fields - Data elements as defined by the bitmap
Data Type Details
BCD (Binary Coded Decimal)
- Each byte contains 2 digits (nibbles)
-
"1234" becomes
0x12 0x34 - Odd-length values are left-padded with "0" before encoding
Track 2 (Type Z)
- Used for magnetic stripe data (Field 35)
- Similar to BCD but with different padding rules
ASCII
- Direct character representation
-
"1234" is
0x31 0x32 0x33 0x34
Binary
- Raw bytes, no encoding conversion
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.