defmodule DaProductApp.MercuryISO8583.Decoder do import Bitwise @moduledoc """ Binary message decoder for Mercury ISO8583 messages. Handles TPDU extraction, MTI decoding, bitmap parsing, and field extraction. """ alias DaProductApp.MercuryISO8583.{Message, MTI, Bitmap, DataTypes, Utils} @doc """ Decode binary ISO8583 message with optional TPDU. ## Examples iex> Decoder.decode(binary_message) {:ok, %Message{mti: "0800", fields: %{"11" => "646465", "70" => "001"}, ...}} """ def decode(binary_data) when is_binary(binary_data) do with {:ok, {tpdu, iso_data}} <- extract_tpdu(binary_data), {:ok, {mti, bitmap_and_fields}} <- decode_mti(iso_data), {:ok, {bitmap, field_data}} <- decode_bitmap(bitmap_and_fields), {:ok, fields} <- decode_fields(field_data, bitmap) do message = %Message{ mti: mti, tpdu: tpdu, fields: fields, bitmap: bitmap, raw_bitmap: bitmap } {:ok, message} end end @doc """ Extract TPDU from message if present. Mercury TPDU is 5 bytes: 6000782000 """ defp extract_tpdu(<<0x60, 0x00, 0x78, 0x20, 0x00, rest::binary>>) do {:ok, {<<0x60, 0x00, 0x78, 0x20, 0x00>>, rest}} end defp extract_tpdu(binary_data) do {:ok, {nil, binary_data}} end @doc """ Decode MTI from BCD format. MTI is 2 bytes in BCD format (4 digits). """ defp decode_mti(<>) do case MTI.decode_bcd(mti_bcd) do {:ok, mti} -> {:ok, {mti, rest}} {:error, reason} -> {:error, "MTI decode error: #{reason}"} end end defp decode_mti(binary_data) when byte_size(binary_data) < 2 do {:error, "Insufficient data for MTI (need 2 bytes, got #{byte_size(binary_data)})"} end @doc """ Decode bitmap to determine present fields. Bitmap can be 8 bytes (primary) or 16 bytes (extended). """ defp decode_bitmap(<> = data) do # Check if field 1 is set (secondary bitmap present) secondary_bitmap_present = (first_byte &&& 0x80) != 0 bitmap_size = if secondary_bitmap_present, do: 16, else: 8 if byte_size(data) >= bitmap_size do <> = data {:ok, {bitmap, field_data}} else {:error, "Insufficient data for bitmap (need #{bitmap_size} bytes, got #{byte_size(data)})"} end end defp decode_bitmap(binary_data) when byte_size(binary_data) < 8 do {:error, "Insufficient data for bitmap (need at least 8 bytes, got #{byte_size(binary_data)})"} end @doc """ Decode fields based on bitmap and field data. """ defp decode_fields(field_data, bitmap) do case Bitmap.parse(bitmap) do {:ok, field_list} -> decode_field_values(field_data, field_list, %{}) {:error, reason} -> {:error, "Bitmap parse error: #{reason}"} end end @doc """ Recursively decode field values from binary data. """ defp decode_field_values(<<>>, [], acc), do: {:ok, acc} defp decode_field_values(_data, [], acc), do: {:ok, acc} # Extra data remaining defp decode_field_values(<<>>, _fields, _acc), do: {:error, "Insufficient field data"} defp decode_field_values(field_data, [field | remaining_fields], acc) do case DataTypes.unpack_field(field, field_data) do {:ok, {field_value, remaining_data}} -> updated_acc = Map.put(acc, field, field_value) decode_field_values(remaining_data, remaining_fields, updated_acc) {:error, reason} -> {:error, "Field #{field} decode error: #{reason}"} end end @doc """ Decode message with TCP length header. Some systems prepend a 2-byte length header. """ def decode_with_length_header(<>) do expected_length = byte_size(message_data) if length == expected_length do decode(message_data) else {:error, "Length mismatch: header indicates #{length}, but got #{expected_length} bytes"} end end def decode_with_length_header(binary_data) when byte_size(binary_data) < 2 do {:error, "Insufficient data for length header"} end @doc """ Decode message without length header validation. """ def decode_without_length_header(<<_length::16-big, message_data::binary>>) do decode(message_data) end def decode_without_length_header(binary_data) do decode(binary_data) end @doc """ Decode field-by-field for debugging purposes. Returns detailed information about each step of the decoding process. """ def decode_detailed(binary_data) when is_binary(binary_data) do steps = [] with {:ok, {tpdu, iso_data}, steps} <- extract_tpdu_detailed(binary_data, steps), {:ok, {mti, bitmap_and_fields}, steps} <- decode_mti_detailed(iso_data, steps), {:ok, {bitmap, field_data}, steps} <- decode_bitmap_detailed(bitmap_and_fields, steps), {:ok, fields, steps} <- decode_fields_detailed(field_data, bitmap, steps) do message = %Message{ mti: mti, tpdu: tpdu, fields: fields, bitmap: bitmap, raw_bitmap: bitmap } {:ok, message, Enum.reverse(steps)} else {:error, reason, steps} -> {:error, reason, Enum.reverse(steps)} {:error, reason} -> {:error, reason, Enum.reverse(steps)} end end # Detailed decoding functions that track steps defp extract_tpdu_detailed(<<0x60, 0x00, 0x78, 0x20, 0x00, rest::binary>>, steps) do step = %{ action: :extract_tpdu, tpdu: <<0x60, 0x00, 0x78, 0x20, 0x00>>, tpdu_hex: "6000782000", remaining_length: byte_size(rest) } {:ok, {<<0x60, 0x00, 0x78, 0x20, 0x00>>, rest}, [step | steps]} end defp extract_tpdu_detailed(binary_data, steps) do step = %{ action: :extract_tpdu, tpdu: nil, note: "No TPDU found", remaining_length: byte_size(binary_data) } {:ok, {nil, binary_data}, [step | steps]} end defp decode_mti_detailed(<>, steps) do case MTI.decode_bcd(mti_bcd) do {:ok, mti} -> step = %{ action: :decode_mti, mti_bcd: Utils.binary_to_hex(mti_bcd), mti: mti, remaining_length: byte_size(rest) } {:ok, {mti, rest}, [step | steps]} {:error, reason} -> step = %{ action: :decode_mti, mti_bcd: Utils.binary_to_hex(mti_bcd), error: reason } {:error, "MTI decode error: #{reason}", [step | steps]} end end defp decode_mti_detailed(binary_data, steps) do step = %{ action: :decode_mti, error: "Insufficient data for MTI", available_bytes: byte_size(binary_data) } {:error, "Insufficient data for MTI", [step | steps]} end defp decode_bitmap_detailed(<> = data, steps) do secondary_bitmap_present = (first_byte &&& 0x80) != 0 bitmap_size = if secondary_bitmap_present, do: 16, else: 8 if byte_size(data) >= bitmap_size do <> = data case Bitmap.parse(bitmap) do {:ok, field_list} -> step = %{ action: :decode_bitmap, bitmap_hex: Utils.binary_to_hex(bitmap), bitmap_size: bitmap_size, secondary_present: secondary_bitmap_present, present_fields: field_list, remaining_length: byte_size(field_data) } {:ok, {bitmap, field_data}, [step | steps]} {:error, reason} -> step = %{ action: :decode_bitmap, bitmap_hex: Utils.binary_to_hex(bitmap), error: reason } {:error, "Bitmap parse error: #{reason}", [step | steps]} end else step = %{ action: :decode_bitmap, error: "Insufficient data for bitmap", needed_bytes: bitmap_size, available_bytes: byte_size(data) } {:error, "Insufficient data for bitmap", [step | steps]} end end defp decode_fields_detailed(field_data, bitmap, steps) do case Bitmap.parse(bitmap) do {:ok, field_list} -> case decode_field_values_detailed(field_data, field_list, %{}, steps) do {:ok, fields, updated_steps} -> {:ok, fields, updated_steps} {:error, reason, updated_steps} -> {:error, reason, updated_steps} end {:error, reason} -> {:error, "Bitmap parse error: #{reason}", steps} end end defp decode_field_values_detailed(<<>>, [], acc, steps), do: {:ok, acc, steps} defp decode_field_values_detailed(_data, [], acc, steps), do: {:ok, acc, steps} defp decode_field_values_detailed(<<>>, _fields, _acc, steps) do {:error, "Insufficient field data", steps} end defp decode_field_values_detailed(field_data, [field | remaining_fields], acc, steps) do case DataTypes.unpack_field(field, field_data) do {:ok, {field_value, remaining_data}} -> step = %{ action: :decode_field, field: field, value: field_value, consumed_bytes: byte_size(field_data) - byte_size(remaining_data), remaining_length: byte_size(remaining_data) } updated_acc = Map.put(acc, field, field_value) decode_field_values_detailed(remaining_data, remaining_fields, updated_acc, [step | steps]) {:error, reason} -> step = %{ action: :decode_field, field: field, error: reason, available_bytes: byte_size(field_data) } {:error, "Field #{field} decode error: #{reason}", [step | steps]} end end end