defmodule DaProductApp.Switch.UpstreamConnection do @moduledoc """ Handles connections to upstream networks with connection pooling and failover. """ require Logger alias DaProductApp.Acquirer.YSP.YspMessageFraming import Bitwise @timeout 30_000 @doc """ Send a message to an upstream network. """ def send_message(packed_bytes, upstream_config) when is_binary(packed_bytes) do host = Map.get(upstream_config, :host) port = Map.get(upstream_config, :port) timeout = Map.get(upstream_config, :timeout, @timeout) network_name = Map.get(upstream_config, :network_name, "unknown") Logger.debug("Connecting to upstream #{host}:#{port}") Logger.debug("Sending #{byte_size(packed_bytes)} bytes to upstream") Logger.debug("Sending Bytes: #{inspect(packed_bytes)}") # Enhanced hex logging for YSP networks Logger.debug("Network name: #{network_name}, is_ysp?: #{is_ysp_network?(network_name)}") case is_ysp_network?(network_name) do true -> Logger.debug("Using enhanced YSP hex analysis for: #{network_name}") log_ysp_message_analysis(packed_bytes, "Outgoing to #{network_name}") false -> Logger.debug("Sending Hex: #{Base.encode16(packed_bytes)}") end try do case :gen_tcp.connect(String.to_charlist(host), port, [:binary, active: false], timeout) do {:ok, socket} -> result = send_and_receive(socket, packed_bytes, timeout) :gen_tcp.close(socket) result {:error, reason} -> Logger.error("Failed to connect to upstream #{host}:#{port}: #{inspect(reason)}") {:error, {:connection_failed, reason}} end rescue exception -> Logger.error("Exception connecting to upstream: #{inspect(exception)}") {:error, {:connection_exception, exception}} end end @doc """ Test connection to an upstream network. """ def test_connection(upstream_config) do host = Map.get(upstream_config, :host) port = Map.get(upstream_config, :port) timeout = Map.get(upstream_config, :timeout, @timeout) if is_nil(host) or is_nil(port) do Logger.debug("Skipping TCP connection test - no host/port configured (likely gateway network)") :ok else Logger.info("Testing connection to #{host}:#{port}") case :gen_tcp.connect(String.to_charlist(host), port, [:binary, active: false], timeout) do {:ok, socket} -> :gen_tcp.close(socket) Logger.info("Connection test successful for #{host}:#{port}") :ok {:error, reason} -> Logger.error("Connection test failed for #{host}:#{port}: #{inspect(reason)}") {:error, reason} end end end # Private functions defp send_and_receive(socket, data, timeout) do # For YSP networks, the data is already framed by YspMessageFraming # For other networks, send data as-is (framing handled by upstream router) case :gen_tcp.send(socket, data) do :ok -> receive_response(socket, timeout) {:error, reason} -> Logger.error("Failed to send message: #{inspect(reason)}") {:error, {:send_failed, reason}} end end defp receive_response(socket, timeout) do Logger.debug("Waiting for response from upstream...") # YSP server sends complete response as one TCP packet (length + data together) # Just receive whatever comes on the socket as the final complete packet case :gen_tcp.recv(socket, 0, timeout) do {:ok, complete_response} -> Logger.debug("Raw response received (#{byte_size(complete_response)} bytes total):") Logger.debug("Complete raw response hex: #{Base.encode16(complete_response)}") # Parse the length prefix to understand the packet structure case complete_response do <> -> # Decode BCD length: each byte contains two BCD digits bcd_length_bytes = <> decoded_data_length = decode_bcd_length(bcd_length_bytes) expected_total_length = 2 + decoded_data_length # length field (2) + data Logger.debug("Length prefix from response (BCD): #{Base.encode16(bcd_length_bytes)} = #{decoded_data_length} bytes data") Logger.debug("Expected total packet size: #{expected_total_length} bytes (2 length + #{decoded_data_length} data)") Logger.debug("Actual received size: #{byte_size(complete_response)} bytes") _ -> Logger.warning("Response does not have valid BCD length prefix") end {:ok, complete_response} {:error, reason} -> Logger.error("Failed to receive complete response: #{inspect(reason)}") {:error, {:receive_failed, reason}} end end # Helper functions for YSP message analysis defp is_ysp_network?(network_name) when is_binary(network_name) do String.starts_with?(network_name, "ysp") or String.contains?(network_name, "ysp") end defp is_ysp_network?(_), do: false # Helper function for BCD length decoding defp decode_bcd_length(<>) do # Unpack BCD: high nibble and low nibble from each byte d1 = (byte1 >>> 4) &&& 0x0F d2 = byte1 &&& 0x0F d3 = (byte2 >>> 4) &&& 0x0F d4 = byte2 &&& 0x0F # Convert to decimal number d1 * 1000 + d2 * 100 + d3 * 10 + d4 end defp log_ysp_message_analysis(binary_data, context) do try do # Use the enhanced YSP message analysis for better readability case YspMessageFraming.parse_iso8583_structure(binary_data) do {:ok, parsed} -> # Log a compact summary for better log readability summary = format_ysp_summary(parsed) Logger.debug("#{context} - YSP Analysis: #{summary}") # For detailed debugging, log the full analysis (commented out by default) # detailed_analysis = YspMessageFraming.format_iso8583_fields(binary_data, show_raw: false) # Logger.debug("#{context} - Detailed Analysis:\n#{detailed_analysis}") {:error, _reason} -> # Fallback to basic hex if enhanced parsing fails Logger.debug("#{context} - Could not parse as ISO8583, using basic hex") end rescue e -> Logger.warning("YSP message analysis failed: #{inspect(e)}, falling back to basic hex") end end defp format_ysp_summary(parsed) do try do mti = Base.encode16(parsed.mti) # Since we only handle :basic parse method, simplify this bitmap_hex = Base.encode16(parsed.primary_bitmap) |> String.slice(0, 8) fields = Enum.join(Enum.take(parsed.bitmap_bits, 10), ",") "MTI=#{mti} Bitmap=#{bitmap_hex}... Fields=[#{fields}...]" rescue _ -> "MTI=#{Base.encode16(parsed.mti)} (basic)" end end end