defmodule DaProductApp.MercuryISO8583.MTI do @moduledoc """ Message Type Indicator (MTI) handling for Mercury ISO8583 messages. Handles BCD encoding/decoding and MTI validation. """ alias DaProductApp.MercuryISO8583.Utils @doc """ Decode BCD-encoded MTI to string format. Mercury sends MTI in BCD format (2 bytes = 4 digits). ## Examples iex> MTI.decode_bcd(<<0x08, 0x00>>) {:ok, "0800"} iex> MTI.decode_bcd(<<0x02, 0x00>>) {:ok, "0200"} """ def decode_bcd(<>) do mti_string = Utils.bcd_to_string(<>) # Ensure we have exactly 4 digits padded_mti = String.pad_leading(mti_string, 4, "0") case validate_mti(padded_mti) do {:ok, mti} -> {:ok, mti} {:error, reason} -> {:error, "Invalid BCD MTI: #{reason}"} end end def decode_bcd(binary) when byte_size(binary) != 2 do {:error, "MTI BCD data must be exactly 2 bytes, got #{byte_size(binary)} bytes"} end @doc """ Encode string MTI to BCD format. ## Examples iex> MTI.encode_bcd("0800") {:ok, <<0x08, 0x00>>} iex> MTI.encode_bcd("0200") {:ok, <<0x02, 0x00>>} """ def encode_bcd(mti) when is_binary(mti) do case validate_mti(mti) do {:ok, valid_mti} -> bcd_data = Utils.string_to_bcd(valid_mti) {:ok, bcd_data} {:error, reason} -> {:error, "Cannot encode invalid MTI: #{reason}"} end end def encode_bcd(mti) do {:error, "MTI must be a string, got: #{inspect(mti)}"} end @doc """ Validate MTI format and content. ## Examples iex> MTI.validate_mti("0800") {:ok, "0800"} iex> MTI.validate_mti("9999") {:error, "Invalid MTI format"} """ def validate_mti(mti) when is_binary(mti) do cond do String.length(mti) != 4 -> {:error, "MTI must be exactly 4 digits"} not Utils.numeric?(mti) -> {:error, "MTI must contain only numeric characters"} not valid_mti_format?(mti) -> {:error, "Invalid MTI format"} true -> {:ok, mti} end end def validate_mti(_) do {:error, "MTI must be a string"} end @doc """ Check if MTI follows valid ISO8583 format. MTI Structure: VFMO - V: Version (0-9) - F: Message Function (0-9) - M: Message Type (0-9) - O: Origin (0-9) """ defp valid_mti_format?(<>) do valid_version?(v) and valid_function?(f) and valid_message_type?(m) and valid_origin?(o) end defp valid_mti_format?(_), do: false # Version: 0 = ISO 8583:1987, 1 = ISO 8583:1993, 2 = ISO 8583:2003 defp valid_version?(v) when v in ["0", "1", "2"], do: true defp valid_version?(_), do: false # Message Function defp valid_function?(f) when f in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], do: true defp valid_function?(_), do: false # Message Type defp valid_message_type?(m) when m in ["0", "1", "2", "3"], do: true defp valid_message_type?(_), do: false # Message Origin defp valid_origin?(o) when o in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], do: true defp valid_origin?(_), do: false @doc """ Get MTI version. ## Examples iex> MTI.get_version("0800") "0" """ def get_version(<>), do: version def get_version(_), do: nil @doc """ Get MTI message function. ## Examples iex> MTI.get_function("0800") "8" """ def get_function(<<_::binary-size(1), function::binary-size(1), _::binary-size(2)>>), do: function def get_function(_), do: nil @doc """ Get MTI message type. ## Examples iex> MTI.get_message_type("0800") "0" """ def get_message_type(<<_::binary-size(2), type::binary-size(1), _::binary-size(1)>>), do: type def get_message_type(_), do: nil @doc """ Get MTI origin. ## Examples iex> MTI.get_origin("0800") "0" """ def get_origin(<<_::binary-size(3), origin::binary-size(1)>>), do: origin def get_origin(_), do: nil @doc """ Check if MTI is a request message (message type 0 or 2). ## Examples iex> MTI.request?("0800") true iex> MTI.request?("0810") false """ def request?(mti) do get_message_type(mti) in ["0", "2"] end @doc """ Check if MTI is a response message (message type 1 or 3). ## Examples iex> MTI.response?("0810") true iex> MTI.response?("0800") false """ def response?(mti) do get_message_type(mti) in ["1", "3"] end @doc """ Generate response MTI from request MTI. ## Examples iex> MTI.to_response("0800") "0810" iex> MTI.to_response("0200") "0210" """ def to_response(<>) do v <> f <> "1" <> o end def to_response(<>) do v <> f <> "3" <> o end def to_response(mti), do: mti # Already a response or invalid format @doc """ Get message class description from MTI. ## Examples iex> MTI.message_class("0800") "Network Management" iex> MTI.message_class("0200") "Authorization" """ def message_class(mti) do case get_function(mti) do "0" -> "Authorization" "1" -> "Financial" "2" -> "File Action" "3" -> "Reserved" "4" -> "Reversal" "5" -> "Reconciliation" "6" -> "Administrative" "7" -> "Fee Collection" "8" -> "Network Management" "9" -> "Reserved" _ -> "Unknown" end end @doc """ Get message type description from MTI. ## Examples iex> MTI.message_type_description("0800") "Request" iex> MTI.message_type_description("0810") "Request Response" """ def message_type_description(mti) do case get_message_type(mti) do "0" -> "Request" "1" -> "Request Response" "2" -> "Advice" "3" -> "Advice Response" _ -> "Unknown" end end @doc """ Get full MTI description. ## Examples iex> MTI.description("0800") "Network Management Request" """ def description(mti) do class = message_class(mti) type = message_type_description(mti) "#{class} #{type}" end @doc """ Common MTI definitions for Mercury payment processing. """ def common_mtis do %{ "0100" => "Authorization Request", "0110" => "Authorization Response", "0120" => "Authorization Advice", "0130" => "Authorization Advice Response", "0200" => "Financial Request", "0210" => "Financial Response", "0220" => "Financial Advice", "0230" => "Financial Advice Response", "0400" => "Reversal Request", "0410" => "Reversal Response", "0420" => "Reversal Advice", "0430" => "Reversal Advice Response", "0800" => "Network Management Request", "0810" => "Network Management Response", "0820" => "Network Management Advice", "0830" => "Network Management Advice Response" } end @doc """ Check if MTI is in the common Mercury MTI list. """ def mercury_mti?(mti) do Map.has_key?(common_mtis(), mti) end end