defmodule DaProductApp.MercuryISO8583.Utils do import Bitwise @moduledoc """ Utility functions for Mercury ISO8583 message processing. Provides binary operations, field mapping, and common helpers. """ @doc """ Convert field number to string format. """ def field_to_string(field) when is_integer(field), do: Integer.to_string(field) def field_to_string(field) when is_binary(field), do: field def field_to_string(field) when is_atom(field), do: Atom.to_string(field) @doc """ Convert field string to integer. """ def field_to_integer(field) when is_integer(field), do: field def field_to_integer(field) when is_binary(field), do: String.to_integer(field) @doc """ Convert binary to hex string representation. """ def binary_to_hex(binary) when is_binary(binary) do binary |> :binary.bin_to_list() |> Enum.map(&Integer.to_string(&1, 16)) |> Enum.map(&String.pad_leading(&1, 2, "0")) |> Enum.join() |> String.upcase() end @doc """ Convert hex string to binary. """ def hex_to_binary(hex_string) when is_binary(hex_string) do hex_string |> String.upcase() |> String.graphemes() |> Enum.chunk_every(2) |> Enum.map(&Enum.join/1) |> Enum.map(&String.to_integer(&1, 16)) |> :erlang.list_to_binary() rescue _ -> {:error, :invalid_hex} end @doc """ Pad string to specified length with leading zeros. """ def pad_left(value, length) when is_binary(value) do String.pad_leading(value, length, "0") end def pad_left(value, length) when is_integer(value) do value |> Integer.to_string() |> String.pad_leading(length, "0") end @doc """ Pad string to specified length with trailing spaces. """ def pad_right(value, length) when is_binary(value) do String.pad_trailing(value, length, " ") end @doc """ Convert BCD (Binary Coded Decimal) to string. Each byte contains two decimal digits. """ def bcd_to_string(<<>>), do: "" def bcd_to_string(<>) do high_nibble = (byte >>> 4) &&& 0x0F low_nibble = byte &&& 0x0F high_digit = if high_nibble <= 9, do: Integer.to_string(high_nibble), else: "" low_digit = if low_nibble <= 9, do: Integer.to_string(low_nibble), else: "" high_digit <> low_digit <> bcd_to_string(rest) end @doc """ Convert string to BCD (Binary Coded Decimal). Each byte contains two decimal digits. """ def string_to_bcd(string) when is_binary(string) do # Pad to even length if necessary padded = if rem(String.length(string), 2) == 1, do: "0" <> string, else: string padded |> String.graphemes() |> Enum.chunk_every(2) |> Enum.map(fn [high, low] -> high_int = String.to_integer(high) low_int = String.to_integer(low) (high_int <<< 4) ||| low_int [single] -> single_int = String.to_integer(single) single_int <<< 4 end) |> :binary.list_to_bin() end @doc """ Extract TPDU from message if present. Returns {tpdu, remaining_message} or {nil, original_message}. """ def extract_tpdu(<<0x60, 0x00, 0x78, 0x20, 0x00, rest::binary>>) do {<<0x60, 0x00, 0x78, 0x20, 0x00>>, rest} end def extract_tpdu(message) do {nil, message} end @doc """ Check if bit is set in bitmap at given position. Position 1-128 (1-based indexing as per ISO8583 standard). """ def bitmap_bit_set?(bitmap, position) when position >= 1 and position <= 128 do byte_index = div(position - 1, 8) bit_index = rem(position - 1, 8) case :binary.at(bitmap, byte_index) do byte when is_integer(byte) -> (byte &&& (1 <<< (7 - bit_index))) != 0 _ -> false end end @doc """ Set bit in bitmap at given position. Position 1-128 (1-based indexing as per ISO8583 standard). """ def bitmap_set_bit(bitmap, position) when position >= 1 and position <= 128 do byte_index = div(position - 1, 8) bit_index = rem(position - 1, 8) # Ensure bitmap is at least 16 bytes (128 bits) padded_bitmap = :binary.copy(bitmap, 1) <> :binary.copy(<<0>>, max(0, 16 - byte_size(bitmap))) <> = padded_bitmap new_byte = byte ||| (1 <<< (7 - bit_index)) prefix <> <> <> suffix end @doc """ Convert map with string keys to atom keys. """ def atomify_map(map) when is_map(map) do map |> Enum.map(fn {k, v} -> atom_key = if is_binary(k), do: String.to_atom(k), else: k {atom_key, v} end) |> Enum.into(%{}) end @doc """ Convert map with atom keys to string keys. """ def stringify_map(map) when is_map(map) do map |> Enum.map(fn {k, v} -> string_key = if is_atom(k), do: Atom.to_string(k), else: k {string_key, v} end) |> Enum.into(%{}) end @doc """ Check if string contains only numeric characters. """ def numeric?(string) when is_binary(string) do String.match?(string, ~r/^\d+$/) end @doc """ Check if string contains only alphanumeric characters. """ def alphanumeric?(string) when is_binary(string) do String.match?(string, ~r/^[a-zA-Z0-9]+$/) end @doc """ Field name to number mapping for common ISO8583 fields. """ def field_name_to_number do %{ :mti => "0", :pan => "2", :processing_code => "3", :amount => "4", :settlement_amount => "6", :transmission_datetime => "7", :stan => "11", :local_time => "12", :local_date => "13", :expiry_date => "14", :settlement_date => "15", :merchant_type => "18", :pos_entry_mode => "22", :card_sequence => "23", :function_code => "24", :pos_condition_code => "25", :pos_capture_code => "26", :auth_id_length => "27", :amount_fee => "28", :acquiring_institution_id => "32", :forwarding_institution_id => "33", :track2 => "35", :retrieval_reference => "37", :auth_id => "38", :response_code => "39", :service_restriction_code => "40", :terminal_id => "41", :merchant_id => "42", :additional_response => "44", :track1 => "45", :additional_amounts => "54", :emv_data => "55", :echo_data => "59", :network_management => "70" } end @doc """ Field number to name mapping for common ISO8583 fields. """ def field_number_to_name do field_name_to_number() |> Enum.map(fn {name, number} -> {number, name} end) |> Enum.into(%{}) end @doc """ Get field name from number or vice versa. """ def field_alias(field) when is_atom(field) do field_name_to_number()[field] || Atom.to_string(field) end def field_alias(field) when is_binary(field) do field_number_to_name()[field] || field end def field_alias(field) when is_integer(field) do field_number_to_name()[Integer.to_string(field)] || Integer.to_string(field) end end