defmodule DaProductApp.Switch.Protocol do @moduledoc """ Ranch protocol handler for ISO8583 TCP connections. Handles connection lifecycle and message framing with configurable message handlers. """ @behaviour :ranch_protocol require Logger alias DaProductApp.Switch.Config alias DaProductApp.MercuryISO8583.Packagers.ISOMsg def start_link(ref, transport, opts) do pid = spawn_link(__MODULE__, :init, [ref, transport, opts]) {:ok, pid} end def init(ref, transport, _opts) do {:ok, socket} = :ranch.handshake(ref) :ok = transport.setopts(socket, [{:active, :once}]) # Get configured message handler message_handler = Config.get_message_handler() Logger.info("New ISO8583 connection established using handler: #{inspect(message_handler)}") loop(socket, transport, <<>>, message_handler) end defp loop(socket, transport, buffer, message_handler) do receive do {data_msg, ^socket, data} when data_msg in [:tcp, :ssl] -> # Log raw data if configured if should_log_raw_data?() do Logger.info("Raw data received (#{byte_size(data)} bytes): #{inspect(data, limit: :infinity)}") Logger.info("Raw data as hex: #{Base.encode16(data)}") end new_buffer = buffer <> data Logger.debug("Total buffer size: #{byte_size(new_buffer)} bytes") {processed_buffer, responses} = process_messages(new_buffer, message_handler) # Send responses back to client Enum.each(responses, fn response -> transport.send(socket, response) end) :ok = transport.setopts(socket, [{:active, :once}]) loop(socket, transport, processed_buffer, message_handler) {closed_msg, ^socket} when closed_msg in [:tcp_closed, :ssl_closed] -> Logger.info("ISO8583 connection closed") :ok {error_msg, ^socket, reason} when error_msg in [:tcp_error, :ssl_error] -> Logger.error("ISO8583 connection error: #{inspect(reason)}") :ok after get_connection_timeout() -> Logger.info("ISO8583 connection timeout, closing") transport.close(socket) end end defp process_messages(buffer, message_handler, responses \\ []) defp process_messages(<<>>, _message_handler, responses), do: {<<>>, Enum.reverse(responses)} #Called function override defp process_messages(buffer, message_handler, responses) do # Log the buffer content before attempting to parse Logger.debug("Processing buffer (#{byte_size(buffer)} bytes) with #{inspect(message_handler)}") if should_log_raw_data?() do Logger.debug("Buffer as hex: #{Base.encode16(buffer)}") end # Attempt to parse messages from buffer using configured handler case parse_with_handler(message_handler, buffer) do {:ok, message, remaining} -> # Successfully parsed a message mti = ISOMsg.get_mti(message) Logger.info("Received ISO8583 message: MTI #{mti}") #dump the message if logging enabled Logger.info(ISOMsg.dump_iso(message)) # Route the message and get response response_message = route_message(message) # Encode the response using the same handler case encode_with_handler(message_handler, response_message) do {:ok, encoded_response} -> process_messages(remaining, message_handler, [encoded_response | responses]) {:error, reason} -> Logger.error("Failed to encode response: #{inspect(reason)}") # Create a generic error response error_response = create_error_response(message, message_handler) case encode_with_handler(message_handler, error_response) do {:ok, encoded_error} -> process_messages(remaining, message_handler, [encoded_error | responses]) {:error, _} -> process_messages(remaining, message_handler, responses) end end {:incomplete, _} -> # Need more data {buffer, Enum.reverse(responses)} {:error, reason} -> Logger.error("Failed to parse ISO8583 message with #{inspect(message_handler)}: #{inspect(reason)}") # Try fallback handler if configured and available case try_fallback_handler(buffer, message_handler) do {:ok, fallback_result} -> Logger.info("Fallback handler succeeded") fallback_result {:error, _fallback_reason} -> Logger.error("Fallback handler also failed, discarding buffer") {<<>>, Enum.reverse(responses)} end end end defp create_error_response(%ISOMsg{} = original_message, message_handler) do try do # Use the handler's create_response function if available case function_exported?(message_handler, :create_response, 3) do true -> message_handler.create_response(original_message, "96", %{}) false -> # Fallback to basic error response creation create_basic_error_response(original_message) end rescue _exception -> Logger.warning("Handler error response creation failed, using basic response") create_basic_error_response(original_message) end end # Legacy support for backward compatibility defp create_error_response(original_message, message_handler) when is_map(original_message) do Logger.warning("Using legacy map format in create_error_response - should use ISOMsg") try do # Use the handler's create_response function if available case function_exported?(message_handler, :create_response, 3) do true -> message_handler.create_response(original_message, "96", %{}) false -> # Fallback to basic error response creation create_basic_error_response(original_message) end rescue _exception -> Logger.warning("Handler error response creation failed, using basic response") create_basic_error_response(original_message) end end defp create_basic_error_response(%ISOMsg{} = original_message) do alias DaProductApp.MercuryISO8583.Packagers.ISOMsg # Convert request MTI to response MTI mti = ISOMsg.get_mti(original_message) response_mti = case mti do "0" <> rest -> "1" <> rest # 0xxx -> 1xxx "2" <> rest -> "3" <> rest # 2xxx -> 3xxx mti -> mti end # Create new ISO message with response MTI error_response = ISOMsg.new(response_mti) # Returns struct directly # Set the same packager as the original message response_with_packager = ISOMsg.set_packager(error_response, ISOMsg.get_packager(original_message)) # Set response code response_with_code = ISOMsg.set(response_with_packager, 39, "96") # Preserve essential fields from original message essential_fields = [11, 7, 41, 42] # STAN, Transmission Date/Time, Terminal ID, Merchant ID final_response = essential_fields |> Enum.reduce(response_with_code, fn field, acc -> case ISOMsg.get_field(original_message, field) do nil -> acc value -> ISOMsg.set(acc, field, value) end end) final_response end # Legacy support defp create_basic_error_response(original_message) when is_map(original_message) do Logger.warning("Using legacy map format in create_basic_error_response - should use ISOMsg") # Convert request MTI to response MTI mti = get_message_mti(original_message) response_mti = case mti do "0" <> rest -> "1" <> rest # 0xxx -> 1xxx "2" <> rest -> "3" <> rest # 2xxx -> 3xxx mti -> mti end error_response = %{ "0" => response_mti, "39" => "96", # System error "message_type_indicator" => response_mti } # Preserve essential fields essential_fields = ["11", "7", "41", "42"] # STAN, Transmission Date/Time, Terminal ID, Merchant ID enhanced_response = essential_fields |> Enum.reduce(error_response, fn field, acc -> case Map.get(original_message, field) do nil -> acc value -> Map.put(acc, field, value) end end) # Preserve TPDU context from original message if present case Map.get(original_message, :tpdu) do nil -> enhanced_response tpdu -> Map.put(enhanced_response, :tpdu, tpdu) end end # Helper functions defp parse_with_handler(message_handler, buffer) do try do message_handler.parse(buffer) rescue exception -> Logger.error("Exception in #{inspect(message_handler)}.parse: #{inspect(exception)}") {:error, {:handler_exception, exception}} end end defp encode_with_handler(message_handler, message) do try do message_handler.encode(message) rescue exception -> Logger.error("Exception in #{inspect(message_handler)}.encode: #{inspect(exception)}") {:error, {:handler_exception, exception}} end end defp route_message(message) do Logger.debug("Routing message with MTI #{get_message_mti(message)}") try do DaProductApp.Switch.Router.route(message) rescue exception -> Logger.error("Exception in message routing: #{inspect(exception)}") create_basic_error_response(message) end end defp get_message_mti(%ISOMsg{} = message) do ISOMsg.get_mti(message) || "0000" end defp get_message_mti(message) when is_map(message) do Map.get(message, "0") || Map.get(message, "message_type_indicator") || "0000" end defp try_fallback_handler(_buffer, _current_handler) do # Fallback is disabled - only use ISO8583BMessageHandler {:error, :fallback_disabled} end defp should_log_raw_data? do Config.get_protocol_config() |> get_in([:logging, :log_raw_data]) |> Kernel.||({false}) end defp get_connection_timeout do Config.get_protocol_config() |> get_in([:tcp, :timeout_ms]) |> Kernel.||({30_000}) end end