README.md

# 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 fields
- `form_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 map
- `bitmap_to_list/1` - Convert bitmap to list of field numbers present
- `list_to_bitmap/1` - Convert field list to binary bitmap
- `split_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 output
- `extract_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` - Numeric
- `a` - Alphabetic
- `an` - Alphanumeric
- `ans` - Alphanumeric + Special
- `b` - Binary
- `z` - Track 2
- `x+n` - Variable length with header

**Length Indicators:**
- `n 6` - Fixed length of 6
- `n ..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:
```elixir
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:

```elixir
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:**
- 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):

```elixir
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:**

```elixir
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:**

```elixir
# 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:**

```elixir
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:**
- **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:**

```elixir
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:

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

**Handling Timeout Responses:**

```elixir
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:

```elixir
# 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:
- 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:

```elixir
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:

```elixir
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:

```elixir
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:**
- `: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:

```elixir
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:**
```bash
POST /iso8583
Content-Type: application/json

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

**Response Format (Success):**
```json
{
  "iso_message": "base64_encoded_response",
  "request_id": "same-as-request"
}
```

**Response Format (Error):**
```json
{
  "error": "error_message",
  "request_id": "same-as-request"
}
```

```elixir
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:**
```elixir
transport_opts: [
  port: 8443,
  scheme: :https,
  certfile: "/path/to/cert.pem",
  keyfile: "/path/to/key.pem"
]
```

**HTTP Context Metadata:**
- `transport_ref` - The `Plug.Conn` struct
- `client_id` - `"http_client"`
- `peer_address` - Client's IP from `conn.remote_ip`
- `transport_metadata` - `%{method, path, headers, user_agent, content_type}`

### Iso8583.Context

The context carries transport-specific metadata alongside messages:

```elixir
# 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:

```elixir
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.).

```elixir
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:
- Binary MTI (4 bytes)
- Binary bitmap (8 or 16 bytes)
- BCD/ASCII encoded fields

```elixir
# 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

```elixir
# 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:

```elixir
# 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

```elixir
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

```elixir
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

```elixir
# 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:

```elixir
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"
end
```

**Key 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

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

## Usage

### Message Type Configuration

Define your message type format:

```elixir
# 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:

```elixir
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:

```elixir
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:

```elixir
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

```elixir
# 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

```elixir
# 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)
- 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:

```bash
mix test
```

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

## License

See [LICENSE](LICENSE) file for details.