defmodule DaProductApp.Switch.Router do @moduledoc """ Routes ISO8583 messages to upstream networks via NetworkManager. This router uses the new network packager infrastructure for: - Direct upstream communication with VISA/MasterCard networks - Proper ISO8583 message formatting using network-specific packagers - BIN-based routing with automatic network detection - Connection management and failover handling """ require Logger alias DaProductApp.MercuryISO8583.Packagers.ISOMsg alias DaProductApp.MercuryISO8583.{NetworkManager, Message} alias DaProductApp.Switch.Config @spec route(map() | ISOMsg.t()) :: map() def route(%ISOMsg{} = iso_message) do # Direct routing for ISOMsg structs (new path) mti = ISOMsg.get_mti(iso_message) Logger.info("Routing ISO8583 message MTI: #{mti}") Logger.debug("Routing message with MTI #{mti}") # Route directly with ISOMsg - no need for legacy conversion case NetworkManager.route_message(iso_message) do {:ok, response_message} -> Logger.info("Received response from upstream network") response_message {:error, :no_route_found} -> Logger.warning("No route found for message, using fallback response") create_iso_error_response(iso_message, "15") # No such issuer {:error, :network_unavailable} -> Logger.error("Network unavailable, creating timeout response") create_iso_error_response(iso_message, "91") # Issuer switch inoperative {:error, reason} -> Logger.error("NetworkManager routing failed: #{inspect(reason)}") create_iso_error_response(iso_message, "96") # System error end end def route(message) when is_map(message) do # Legacy routing for map format (backward compatibility) mti = Map.get(message, "0", "unknown") Logger.info("Routing ISO8583 message MTI: #{mti}") Logger.debug("Routing legacy message with MTI #{mti}") # Convert legacy message format to Mercury ISO8583 Message case convert_to_mercury_message(message) do {:ok, mercury_message} -> route_via_network_manager(mercury_message, message) {:error, reason} -> Logger.error("Failed to convert message format: #{inspect(reason)}") create_error_response(message, "96") # System error end end @doc """ Route message via NetworkManager to upstream networks. """ defp route_via_network_manager(mercury_message, %ISOMsg{} = original_message) do case NetworkManager.route_message(mercury_message) do {:ok, response_message} -> Logger.info("Received response from upstream network") convert_from_mercury_message(response_message) {:error, :no_route_found} -> Logger.warning("No route found for message, using fallback response") create_iso_error_response(original_message, "15") # No such issuer {:error, :network_unavailable} -> Logger.error("Network unavailable, creating timeout response") create_iso_error_response(original_message, "91") # Issuer switch inoperative {:error, reason} -> Logger.error("NetworkManager routing failed: #{inspect(reason)}") create_iso_error_response(original_message, "96") # System error end end defp route_via_network_manager(mercury_message, original_message) when is_map(original_message) do case NetworkManager.route_message(mercury_message) do {:ok, response_message} -> Logger.info("Received response from upstream network") convert_from_mercury_message(response_message) {:error, :no_route_found} -> Logger.warning("No route found for message, using fallback response") create_error_response(original_message, "15") # No such issuer {:error, :network_unavailable} -> Logger.error("Network unavailable, creating timeout response") create_error_response(original_message, "91") # Issuer switch inoperative {:error, reason} -> Logger.error("NetworkManager routing failed: #{inspect(reason)}") create_error_response(original_message, "96") # System error end end @doc """ Convert ISOMsg to Mercury ISO8583 Message (new path). """ defp iso_to_mercury_message(%ISOMsg{} = iso_message) do try do # Create Mercury message from ISOMsg mercury_message = Message.new(iso_message.mti) # Add all fields from ISOMsg final_message = ISOMsg.get_all_fields(iso_message) |> Enum.reduce(mercury_message, fn {field_num, value}, acc -> Message.set_field(acc, Integer.to_string(field_num), value) end) {:ok, final_message} rescue e -> Logger.error("Error converting ISOMsg to Mercury message: #{inspect(e)}") {:error, :conversion_failed} end end @doc """ Convert legacy message format to Mercury ISO8583 Message. """ defp convert_to_mercury_message(message) when is_map(message) do try do # Extract MTI mti = Map.get(message, "0") # Extract fields (everything except MTI) fields = Map.drop(message, ["0"]) # Create Mercury message mercury_message = Message.new(mti) # Add all fields final_message = Enum.reduce(fields, mercury_message, fn {field, value}, acc -> Message.set_field(acc, field, value) end) {:ok, final_message} rescue e -> Logger.error("Error converting to Mercury message: #{inspect(e)}") {:error, :conversion_failed} end end @doc """ Convert Mercury ISO8583 Message back to legacy format. """ defp convert_from_mercury_message(%Message{} = mercury_message) do # Convert back to legacy map format base_map = %{"0" => mercury_message.mti} Map.merge(base_map, mercury_message.fields) end @doc """ Create error response for ISOMsg format. """ defp create_iso_error_response(%ISOMsg{} = original_message, error_code) do mti = ISOMsg.get_mti(original_message) # Convert request MTI to response MTI response_mti = case mti do "02" <> suffix -> "02" <> "1" <> String.slice(suffix, 1, 1) # 0200 -> 0210 "04" <> suffix -> "04" <> "1" <> String.slice(suffix, 1, 1) # 0400 -> 0410 "08" <> suffix -> "08" <> "1" <> String.slice(suffix, 1, 1) # 0800 -> 0810 _ -> "0210" # Default response end # Create new ISOMsg with response MTI and set packager 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, error_code) # Add echo fields from original message echo_fields = [2, 3, 4, 7, 11, 12, 13, 22, 25, 37, 41, 42] final_response = Enum.reduce(echo_fields, 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 @doc """ Create error response in legacy format. """ defp create_error_response(original_message, error_code) do mti = Map.get(original_message, "0", "0200") # Convert request MTI to response MTI response_mti = case mti do "02" <> suffix -> "02" <> "1" <> String.slice(suffix, 1, 1) # 0200 -> 0210 "04" <> suffix -> "04" <> "1" <> String.slice(suffix, 1, 1) # 0400 -> 0410 "08" <> suffix -> "08" <> "1" <> String.slice(suffix, 1, 1) # 0800 -> 0810 _ -> "0210" # Default response end # Create response with error code original_message |> Map.put("0", response_mti) |> Map.put("39", error_code) # Response code |> copy_echo_fields(original_message) end @doc """ Copy echo fields from request to response. """ defp copy_echo_fields(response, request) do # Fields that should be echoed from request to response echo_fields = ["2", "3", "4", "7", "11", "12", "13", "22", "25", "37", "41", "42"] Enum.reduce(echo_fields, response, fn field, acc -> case Map.get(request, field) do nil -> acc value -> Map.put(acc, field, value) end end) end @doc """ Test routing without executing the call. """ def test_route(message, _options \\ []) do case convert_to_mercury_message(message) do {:ok, mercury_message} -> # Test which network would be selected network_info = NetworkManager.determine_network(mercury_message) {:ok, %{ routing_system: :network_manager, network: network_info[:network], connector: network_info[:connector], routing_reason: network_info[:reason], bin_range: network_info[:bin_range] }} {:error, reason} -> {:error, reason} end end @doc """ Utility functions for message analysis and debugging. """ # Helper function to determine if message is a network management message def is_network_management?(message) when is_map(message) do mti = Map.get(message, "0", "0000") case mti do "08" <> _ -> true # Network management request "18" <> _ -> true # Network management response _ -> false end end # Helper function to determine if message is a transaction request def is_transaction_request?(message) when is_map(message) do mti = Map.get(message, "0", "0000") case mti do "02" <> _ -> true # Financial transaction request "12" <> _ -> true # Financial transaction response "04" <> _ -> true # Reversal transaction request "14" <> _ -> true # Reversal transaction response _ -> false end end # Helper to get transaction type from processing code def get_transaction_type(processing_code) when is_binary(processing_code) and byte_size(processing_code) >= 2 do case String.slice(processing_code, 0, 2) do "00" -> :purchase "01" -> :cash_withdrawal "20" -> :refund "31" -> :balance_inquiry "40" -> :transfer _ -> :unknown end end def get_transaction_type(_), do: :unknown @doc """ Get card type from PAN for routing decisions (for analysis purposes only). NetworkManager now handles all routing decisions based on BIN ranges. """ def get_card_type(pan) when is_binary(pan) and byte_size(pan) >= 6 do case pan do "4" <> _ -> "visa" "5" <> _ -> "mastercard" "2" <> _ -> "mastercard" "34" <> _ -> "amex" "37" <> _ -> "amex" "6011" <> _ -> "discover" "644" <> _ -> "discover" "65" <> _ -> "discover" "35" <> _ -> "jcb" "30" <> _ -> "diners" "36" <> _ -> "diners" "38" <> _ -> "diners" _ -> "unknown" end end def get_card_type(_), do: "unknown" end