# ISO8583 Payment Switch Implementation Guide

## Introduction

This document outlines the implementation of an ISO8583-based payment switch system for the Mercury Device Middleware application. The system will act as a middleman that receives ISO8583 payment requests from point-of-sale devices over TCP connections, parses the messages, and routes them to appropriate upstream payment networks (Master/Mastercard, Visa, etc.) based on card type and transaction processing codes.

The switch will handle the complete request-response cycle, including error handling and timeout management, providing a robust and scalable payment processing gateway.

## Architecture Pattern

### Strategy Pattern Implementation

The solution uses the **Strategy Pattern** combined with a **Router Pattern** to achieve:

- **Pluggable connectors**: Each payment network (Master, Visa) implements a common behavior interface
- **Runtime routing**: Messages are routed to appropriate connectors based on card type and processing codes
- **Clean separation**: Protocol handling, message parsing, routing, and network connectors are separated into distinct modules
- **Extensibility**: New payment networks can be added by implementing the connector behavior

### Key Components

1. **TCP Protocol Handler**: Manages incoming TCP connections using Ranch
2. **Message Handler**: Parses and encodes ISO8583 messages using the `iso_8583` library
3. **Router**: Determines which connector to use based on message content
4. **Connectors**: Handle communication with specific payment networks
5. **Supervisor Tree**: Ensures fault tolerance and proper OTP supervision

## Dependencies Required

The following dependencies are already added to `mix.exs`:
- `{:ranch, "~> 2.1"}` - For TCP socket handling
- `{:iso_8583, "~> 0.1.2"}` - For ISO8583 message parsing/encoding

## File and Folder Structure

```
lib/da_product_app/
├── switch/
│   ├── protocol.ex                 # Ranch TCP protocol handler
│   ├── message_handler.ex          # ISO8583 parsing/encoding
│   ├── router.ex                   # Message routing logic
│   └── connectors/
│       ├── behaviour.ex            # Connector behavior definition
│       ├── master.ex               # Master/Mastercard connector
│       ├── visa.ex                 # Visa connector
│       └── fallback.ex             # Default fallback connector
└── switch.ex                       # Main switch module
```

## Implementation Details

### 1. Main Switch Module

**File**: `lib/da_product_app/switch.ex`

```elixir
defmodule DaProductApp.Switch do
  @moduledoc """
  Main entry point for the ISO8583 switch system.
  """

  def start_listener(port \\ 5000) do
    :ranch.start_listener(
      :iso8583_listener,
      100,
      :ranch_tcp,
      [port: port],
      DaProductApp.Switch.Protocol,
      []
    )
  end

  def stop_listener do
    :ranch.stop_listener(:iso8583_listener)
  end
end
```

### 2. TCP Protocol Handler

**File**: `lib/da_product_app/switch/protocol.ex`

```elixir
defmodule DaProductApp.Switch.Protocol do
  @moduledoc """
  Ranch protocol handler for ISO8583 TCP connections.
  Handles connection lifecycle and message framing.
  """
  
  @behaviour :ranch_protocol
  require Logger

  def start_link(ref, socket, transport, _opts) do
    pid = spawn_link(__MODULE__, :init, [ref, socket, transport])
    {:ok, pid}
  end

  def init(ref, socket, transport) do
    :ok = :ranch.accept_ack(ref)
    transport.setopts(socket, active: :once)
    Logger.info("New ISO8583 connection established")
    loop(socket, transport, <<>>)
  end

  defp loop(socket, transport, buffer) do
    receive do
      {:tcp, ^socket, data} ->
        new_buffer = buffer <> data
        
        case parse_message(new_buffer) do
          {:ok, message, remaining} ->
            handle_message(socket, transport, message)
            transport.setopts(socket, active: :once)
            loop(socket, transport, remaining)
            
          {:incomplete, _} ->
            transport.setopts(socket, active: :once)
            loop(socket, transport, new_buffer)
            
          {:error, reason} ->
            Logger.error("Failed to parse ISO8583 message: #{inspect(reason)}")
            transport.close(socket)
        end

      {:tcp_closed, ^socket} ->
        Logger.info("ISO8583 connection closed")
        :ok

      {:tcp_error, ^socket, reason} ->
        Logger.error("ISO8583 connection error: #{inspect(reason)}")
        :ok
    end
  end

  defp parse_message(data) do
    DaProductApp.Switch.MessageHandler.parse(data)
  end

  defp handle_message(socket, transport, message) do
    try do
      response = DaProductApp.Switch.Router.route(message)
      encoded_response = DaProductApp.Switch.MessageHandler.encode(response)
      transport.send(socket, encoded_response)
    rescue
      error ->
        Logger.error("Error handling message: #{inspect(error)}")
        error_response = DaProductApp.Switch.MessageHandler.create_error_response(message)
        transport.send(socket, error_response)
    end
  end
end
```

### 3. Message Handler

**File**: `lib/da_product_app/switch/message_handler.ex`

```elixir
defmodule DaProductApp.Switch.MessageHandler do
  @moduledoc """
  Handles ISO8583 message parsing and encoding using iso_8583 library.
  Implements message framing with length headers.
  """
  
  require Logger

  def parse(data) do
    case extract_length_and_message(data) do
      {:ok, message_data, remaining} ->
        case ISO8583.decode(message_data) do
          {:ok, parsed_message} ->
            {:ok, parsed_message, remaining}
          {:error, reason} ->
            Logger.error("ISO8583 decode error: #{inspect(reason)}")
            {:error, reason}
        end
      {:incomplete, _} ->
        {:incomplete, data}
      {:error, reason} ->
        {:error, reason}
    end
  end

  def encode(message) do
    case ISO8583.encode(message) do
      {:ok, encoded} ->
        # Add 2-byte length header (network byte order)
        length = byte_size(encoded)
        <<length::16>> <> encoded
      {:error, reason} ->
        Logger.error("Failed to encode ISO8583 message: #{inspect(reason)}")
        <<>>
    end
  end

  def create_error_response(original_message) do
    # Create error response using iso_8583 library
    response_mti = case Map.get(original_message, :message_type_indicator) do
      <<"01", rest::binary-size(2)>> -> "11" <> rest  # 0100 -> 0110
      <<"02", rest::binary-size(2)>> -> "21" <> rest  # 0200 -> 0210  
      <<"04", rest::binary-size(2)>> -> "41" <> rest  # 0400 -> 0410
      _ -> "0110" # Default response
    end

    error_message = %ISO8583.Message{
      message_type_indicator: response_mti,
      bitmap: <<>>,
      data_elements: %{
        2 => Map.get(original_message.data_elements, 2, ""), # PAN
        3 => Map.get(original_message.data_elements, 3, "000000"), # Processing code
        11 => Map.get(original_message.data_elements, 11, "000001"), # STAN
        39 => "96", # Response code - System error
        41 => Map.get(original_message.data_elements, 41, ""), # Terminal ID
        42 => Map.get(original_message.data_elements, 42, "") # Merchant ID
      }
    }

    encode(error_message)
  end

  # Private helper functions for message framing
  defp extract_length_and_message(<<length::16, message::binary-size(length), remaining::binary>>) do
    {:ok, message, remaining}
  end

  defp extract_length_and_message(<<length::16, partial::binary>>) when byte_size(partial) < length do
    {:incomplete, <<length::16>> <> partial}
  end

  defp extract_length_and_message(data) when byte_size(data) < 2 do
    {:incomplete, data}
  end

  defp extract_length_and_message(_data) do
    {:error, :invalid_format}
  end
end
```

### 4. Router Implementation

**File**: `lib/da_product_app/switch/router.ex`

```elixir
defmodule DaProductApp.Switch.Router do
  @moduledoc """
  Routes ISO8583 messages to appropriate connectors based on:
  - Processing code (field 3)
  - Primary Account Number (field 2) - for card type detection
  - Message Type Indicator
  """
  
  require Logger

  @spec route(ISO8583.Message.t()) :: ISO8583.Message.t()
  def route(%ISO8583.Message{} = message) do
    Logger.info("Routing ISO8583 message MTI: #{message.message_type_indicator}")
    
    connector = determine_connector(message)
    connector.handle(message)
  end

  defp determine_connector(%ISO8583.Message{data_elements: data_elements} = message) do
    processing_code = Map.get(data_elements, 3, "000000") # Field 3
    pan = Map.get(data_elements, 2, "") # Field 2 - Primary Account Number
    
    case {processing_code, get_card_type(pan)} do
      # Purchase transactions
      {"00" <> _, _} -> DaProductApp.Switch.Connectors.Master
      # Cash withdrawal
      {"01" <> _, _} -> DaProductApp.Switch.Connectors.Master
      # Balance inquiry  
      {"31" <> _, _} -> DaProductApp.Switch.Connectors.Master
      # Visa cards
      {_, "visa"} -> DaProductApp.Switch.Connectors.Visa
      # Mastercard
      {_, "mastercard"} -> DaProductApp.Switch.Connectors.Master
      # Default fallback
      _ -> DaProductApp.Switch.Connectors.Fallback
    end
  end

  defp get_card_type(pan) when is_binary(pan) do
    case pan do
      <<"4", _::binary>> -> "visa"
      <<"5", _::binary>> -> "mastercard"
      <<"2", _::binary>> -> "mastercard"  # Some Mastercard ranges start with 2
      _ -> "unknown"
    end
  end

  defp get_card_type(_), do: "unknown"
end
```

### 5. Connector Behavior

**File**: `lib/da_product_app/switch/connectors/behaviour.ex`

```elixir
defmodule DaProductApp.Switch.Connectors.Behaviour do
  @moduledoc """
  Behaviour definition for payment network connectors.
  Each connector must implement these callbacks.
  """

  @callback handle(message :: ISO8583.Message.t()) :: ISO8583.Message.t()
  @callback connect() :: {:ok, pid()} | {:error, term()}
  @callback disconnect(connection :: pid()) :: :ok
end
```

### 6. Master/Mastercard Connector

**File**: `lib/da_product_app/switch/connectors/master.ex`

```elixir
defmodule DaProductApp.Switch.Connectors.Master do
  @moduledoc """
  Connector for Master/Mastercard switch using iso_8583 library.
  Handles TCP communication with upstream Mastercard network.
  """
  
  @behaviour DaProductApp.Switch.Connectors.Behaviour
  require Logger

  @impl true
  def handle(%ISO8583.Message{} = message) do
    Logger.info("Routing to Master switch - MTI: #{message.message_type_indicator}")
    
    config = Application.get_env(:da_product_app, :switch)[:master]
    
    case send_to_upstream(message, config) do
      {:ok, response} -> response
      {:error, reason} -> 
        Logger.error("Master switch error: #{inspect(reason)}")
        create_error_response(message, "91") # Issuer unavailable
    end
  end

  @impl true
  def connect do
    config = Application.get_env(:da_product_app, :switch)[:master]
    :gen_tcp.connect(
      String.to_charlist(config[:host]), 
      config[:port], 
      [:binary, active: false, packet: 0]
    )
  end

  @impl true
  def disconnect(socket) do
    :gen_tcp.close(socket)
  end

  defp send_to_upstream(%ISO8583.Message{} = message, config) do
    case connect() do
      {:ok, socket} ->
        try do
          encoded = DaProductApp.Switch.MessageHandler.encode(message)
          :ok = :gen_tcp.send(socket, encoded)
          
          case :gen_tcp.recv(socket, 0, config[:timeout] || 30_000) do
            {:ok, response_data} ->
              case DaProductApp.Switch.MessageHandler.parse(response_data) do
                {:ok, response, _} -> {:ok, response}
                {:error, reason} -> {:error, reason}
              end
            {:error, reason} -> {:error, reason}
          end
        after
          disconnect(socket)
        end
      {:error, reason} -> {:error, reason}
    end
  end

  defp create_error_response(%ISO8583.Message{} = original_message, response_code) do
    # Convert request MTI to response MTI
    response_mti = case original_message.message_type_indicator do
      <<"01", rest::binary-size(2)>> -> "11" <> rest
      <<"02", rest::binary-size(2)>> -> "21" <> rest  
      <<"04", rest::binary-size(2)>> -> "41" <> rest
      _ -> "0110"
    end

    %ISO8583.Message{
      message_type_indicator: response_mti,
      bitmap: <<>>,
      data_elements: %{
        2 => Map.get(original_message.data_elements, 2, ""), # PAN
        3 => Map.get(original_message.data_elements, 3, "000000"), # Processing code
        11 => Map.get(original_message.data_elements, 11, "000001"), # STAN
        39 => response_code, # Response code
        41 => Map.get(original_message.data_elements, 41, ""), # Terminal ID
        42 => Map.get(original_message.data_elements, 42, "") # Merchant ID
      }
    }
  end
end
```

### 7. Visa Connector

**File**: `lib/da_product_app/switch/connectors/visa.ex`

```elixir
defmodule DaProductApp.Switch.Connectors.Visa do
  @moduledoc """
  Connector for Visa switch using iso_8583 library.
  Handles TCP communication with upstream Visa network.
  """
  
  @behaviour DaProductApp.Switch.Connectors.Behaviour
  require Logger

  @impl true
  def handle(%ISO8583.Message{} = message) do
    Logger.info("Routing to Visa switch - MTI: #{message.message_type_indicator}")
    
    config = Application.get_env(:da_product_app, :switch)[:visa]
    
    case send_to_upstream(message, config) do
      {:ok, response} -> response
      {:error, reason} -> 
        Logger.error("Visa switch error: #{inspect(reason)}")
        create_error_response(message, "91")
    end
  end

  @impl true
  def connect do
    config = Application.get_env(:da_product_app, :switch)[:visa]
    :gen_tcp.connect(
      String.to_charlist(config[:host]), 
      config[:port], 
      [:binary, active: false, packet: 0]
    )
  end

  @impl true
  def disconnect(socket) do
    :gen_tcp.close(socket)
  end

  defp send_to_upstream(%ISO8583.Message{} = message, config) do
    case connect() do
      {:ok, socket} ->
        try do
          encoded = DaProductApp.Switch.MessageHandler.encode(message)
          :ok = :gen_tcp.send(socket, encoded)
          
          case :gen_tcp.recv(socket, 0, config[:timeout] || 30_000) do
            {:ok, response_data} ->
              case DaProductApp.Switch.MessageHandler.parse(response_data) do
                {:ok, response, _} -> {:ok, response}
                {:error, reason} -> {:error, reason}
              end
            {:error, reason} -> {:error, reason}
          end
        after
          disconnect(socket)
        end
      {:error, reason} -> {:error, reason}
    end
  end

  defp create_error_response(%ISO8583.Message{} = original_message, response_code) do
    response_mti = case original_message.message_type_indicator do
      <<"01", rest::binary-size(2)>> -> "11" <> rest
      <<"02", rest::binary-size(2)>> -> "21" <> rest  
      <<"04", rest::binary-size(2)>> -> "41" <> rest
      _ -> "0110"
    end

    %ISO8583.Message{
      message_type_indicator: response_mti,
      bitmap: <<>>,
      data_elements: %{
        2 => Map.get(original_message.data_elements, 2, ""),
        3 => Map.get(original_message.data_elements, 3, "000000"),
        11 => Map.get(original_message.data_elements, 11, "000001"),
        39 => response_code,
        41 => Map.get(original_message.data_elements, 41, ""),
        42 => Map.get(original_message.data_elements, 42, "")
      }
    }
  end
end
```

### 8. Fallback Connector

**File**: `lib/da_product_app/switch/connectors/fallback.ex`

```elixir
defmodule DaProductApp.Switch.Connectors.Fallback do
  @moduledoc """
  Fallback connector for unrecognized transactions.
  Returns appropriate error responses for unsupported card types or processing codes.
  """
  
  @behaviour DaProductApp.Switch.Connectors.Behaviour
  require Logger

  @impl true
  def handle(%ISO8583.Message{} = message) do
    Logger.warning("No specific connector found, using fallback: MTI #{message.message_type_indicator}")
    
    response_mti = case message.message_type_indicator do
      <<"01", rest::binary-size(2)>> -> "11" <> rest
      <<"02", rest::binary-size(2)>> -> "21" <> rest  
      <<"04", rest::binary-size(2)>> -> "41" <> rest
      _ -> "0110"
    end

    %ISO8583.Message{
      message_type_indicator: response_mti,
      bitmap: <<>>,
      data_elements: %{
        2 => Map.get(message.data_elements, 2, ""),
        3 => Map.get(message.data_elements, 3, "000000"),
        11 => Map.get(message.data_elements, 11, "000001"),
        39 => "12", # Invalid transaction
        41 => Map.get(message.data_elements, 41, ""),
        42 => Map.get(message.data_elements, 42, "")
      }
    }
  end

  @impl true
  def connect, do: {:error, :not_implemented}

  @impl true
  def disconnect(_), do: :ok
end
```

### 9. Application Integration

**File**: Update `lib/da_product_app/application.ex`

```elixir
defmodule DaProductApp.Application do
  # ...existing code...

  def start(_type, _args) do
    children = [
      # ...existing children...
      
      # Start the ISO8583 switch listener
      {Task, fn -> 
        case DaProductApp.Switch.start_listener() do
          :ok -> :ok
          {:error, reason} -> 
            Logger.error("Failed to start ISO8583 listener: #{inspect(reason)}")
            :ok
        end
      end}
    ]

    # ...existing code...
  end
end
```

### 10. Configuration Updates

**File**: Update `config/config.exs`

```elixir
# ...existing config...

config :da_product_app, :switch,
  master: [
    host: "mastercard-switch.example.com",
    port: 7001,
    timeout: 30_000
  ],
  visa: [
    host: "visa-switch.example.com", 
    port: 7002,
    timeout: 30_000
  ],
  listener: [
    port: 5000,
    max_connections: 100
  ]
```

**File**: Update `config/dev.exs`

```elixir
# ...existing dev config...

config :da_product_app, :switch,
  master: [
    host: "localhost",
    port: 7001,
    timeout: 30_000
  ],
  visa: [
    host: "localhost", 
    port: 7002,
    timeout: 30_000
  ]
```

## Routing and Management

### Message Routing Logic

1. **Incoming Message**: Device connects to port 5000 and sends ISO8583 message
2. **Parsing**: Message is parsed using the `iso_8583` library
3. **Routing Decision**: Router examines:
   - Field 3 (Processing Code): Determines transaction type
   - Field 2 (PAN): Determines card network based on BIN ranges
4. **Connector Selection**: Appropriate connector is selected based on routing rules
5. **Upstream Processing**: Message is forwarded to upstream network
6. **Response Handling**: Response is received and forwarded back to device

### Management Features

1. **Connection Management**: Ranch handles TCP connection pooling and lifecycle
2. **Error Handling**: Comprehensive error handling with appropriate ISO8583 response codes
3. **Timeout Management**: Configurable timeouts for upstream connections
4. **Logging**: Detailed logging for debugging and monitoring
5. **Configuration**: Environment-specific configuration for different networks

### Processing Codes and Card Type Mapping

```elixir
# Processing Codes (Field 3)
"00xxxx" -> Purchase transactions
"01xxxx" -> Cash withdrawal
"31xxxx" -> Balance inquiry
"20xxxx" -> Refund transactions

# Card Type Detection (Field 2 - PAN)
"4xxxxxx..." -> Visa
"5xxxxxx..." -> Mastercard
"2xxxxxx..." -> Mastercard (new ranges)
```

## Testing and Deployment

### Testing Strategy

1. **Unit Tests**: Test individual components (message parsing, routing logic)
2. **Integration Tests**: Test complete message flow
3. **Load Testing**: Verify performance under concurrent connections
4. **Network Simulation**: Test with mock upstream switches

### Deployment Considerations

1. **Environment Configuration**: Separate configs for dev/staging/prod
2. **Monitoring**: Implement health checks and metrics
3. **Security**: TLS support for upstream connections
4. **Scalability**: Consider connection pooling for high-volume scenarios

## Implementation Checklist

- [ ] Create folder structure: `lib/da_product_app/switch/`
- [ ] Implement main switch module: `switch.ex`
- [ ] Implement TCP protocol handler: `switch/protocol.ex`
- [ ] Implement message handler: `switch/message_handler.ex`
- [ ] Implement router: `switch/router.ex`
- [ ] Create connectors folder: `switch/connectors/`
- [ ] Implement connector behaviour: `connectors/behaviour.ex`
- [ ] Implement Master connector: `connectors/master.ex`
- [ ] Implement Visa connector: `connectors/visa.ex`
- [ ] Implement fallback connector: `connectors/fallback.ex`
- [ ] Update application.ex to start the switch
- [ ] Add configuration to config files
- [ ] Test basic connectivity and message parsing
- [ ] Implement unit tests
- [ ] Test with mock upstream servers
- [ ] Configure production endpoints

## Notes for Developer

1. **ISO8583 Library**: Use the momentpay `iso_8583` library for all message parsing/encoding
2. **Error Handling**: Always return proper ISO8583 response messages, even for errors
3. **Logging**: Include detailed logging for debugging payment flows
4. **Configuration**: Make all endpoints and timeouts configurable
5. **Security**: Consider implementing TLS for upstream connections in production
6. **Performance**: Monitor connection counts and message throughput
7. **Testing**: Create comprehensive tests including edge cases and error scenarios

This implementation provides a robust, scalable ISO8583 payment switch that can handle multiple payment networks while maintaining clean separation of concerns and following Elixir/OTP best practices.
