defmodule ISO8583.Parser do @moduledoc """ Parses ISO87B BCD-packed ISO8583 response packets from jPOS server. """ import Bitwise @doc """ Parses a complete ISO8583 response packet. ## Returns {:ok, parsed_map} | {:error, reason} """ def parse(response) when byte_size(response) < 9 do {:error, {:too_short, "Response too short: #{byte_size(response)} bytes"}} end def parse(response) do try do <> = response mti = decode_bcd(mti_bcd, 4) present_fields = parse_bitmap(bitmap) parsed = %{ length: length, tpdu: Base.encode16(tpdu), mti: mti, bitmap: Base.encode16(bitmap), present_fields: present_fields, fields: %{} } # Parse individual fields {parsed_fields, _remaining} = parse_fields(fields, present_fields, mti) {:ok, Map.put(parsed, :fields, parsed_fields)} rescue e -> {:error, {:parse_error, Exception.message(e)}} end end @doc """ Extracts specific field value from parsed response. """ def get_field(parsed_response, field_num) do Map.get(parsed_response.fields, field_num) end # Decodes bitmap to list of present field numbers. defp parse_bitmap(bitmap) do bits = for <>, do: bit bits |> Enum.with_index(1) |> Enum.filter(fn {bit, _} -> bit == 1 end) |> Enum.map(fn {_, idx} -> idx end) end # Parses individual fields based on bitmap. # Returns {fields_map, remaining_binary} defp parse_fields(binary, present_fields, mti) do present_fields |> Enum.sort() |> Enum.reduce({%{}, binary}, fn field_num, {acc_map, remaining} -> case parse_field(field_num, remaining, mti) do {:ok, value, rest} -> {Map.put(acc_map, field_num, value), rest} {:error, _} -> {Map.put(acc_map, field_num, nil), remaining} end end) end # Parses a single field based on its number and format. defp parse_field(field_num, binary, _mti) do case field_num do 2 -> parse_llvar_bcd(binary) # PAN 3 -> parse_fixed_bcd(binary, 3) # Processing code 4 -> parse_fixed_bcd(binary, 6) # Amount 11 -> parse_fixed_bcd(binary, 3) # STAN 12 -> parse_fixed_bcd(binary, 3) # Time 13 -> parse_fixed_bcd(binary, 2) # Date 14 -> parse_fixed_bcd(binary, 2) # Expiry 22 -> parse_fixed_bcd(binary, 2) # POS entry mode 23 -> parse_fixed_bcd(binary, 2) # Card sequence 24 -> parse_fixed_bcd(binary, 2) # NII 25 -> parse_fixed_bcd(binary, 1) # POS condition code 35 -> parse_llvar_bcd(binary) # Track 2 37 -> parse_fixed_ascii(binary, 12) # Retrieval reference number 38 -> parse_fixed_ascii(binary, 6) # Approval code 39 -> parse_fixed_ascii(binary, 2) # Response code 41 -> parse_fixed_ascii(binary, 8) # Terminal ID 42 -> parse_fixed_ascii(binary, 15) # Merchant ID 49 -> parse_fixed_ascii(binary, 3) # Currency code _ -> {:error, :unknown_field} end end # Field format parsers defp parse_fixed_bcd(binary, byte_count) when byte_size(binary) >= byte_count do <> = binary value = decode_bcd(data, byte_count * 2) {:ok, value, rest} end defp parse_fixed_bcd(_binary, _byte_count), do: {:error, :insufficient_data} defp parse_fixed_ascii(binary, byte_count) when byte_size(binary) >= byte_count do <> = binary value = String.trim(data) {:ok, value, rest} end defp parse_fixed_ascii(_binary, _byte_count), do: {:error, :insufficient_data} defp parse_llvar_bcd(binary) when byte_size(binary) >= 1 do <> = binary len = decode_bcd(len_bcd, 2) |> String.to_integer() byte_len = div(len + 1, 2) if byte_size(rest) >= byte_len do <> = rest value = decode_bcd(data, len) {:ok, value, remaining} else {:error, :insufficient_data} end end defp parse_llvar_bcd(_binary), do: {:error, :insufficient_data} # Decodes BCD bytes to numeric string. defp decode_bcd(bcd_binary, digit_count) do bcd_binary |> :binary.bin_to_list() |> Enum.flat_map(fn byte -> high = (byte &&& 0xF0) >>> 4 low = byte &&& 0x0F [high, low] end) |> Enum.take(digit_count) |> Enum.map(&Integer.to_string/1) |> Enum.join() end @doc """ Extracts response code (DE39) from parsed response. """ def get_response_code(parsed) do get_field(parsed, 39) end @doc """ Extracts approval code (DE38) from parsed response. """ def get_approval_code(parsed) do get_field(parsed, 38) end @doc """ Extracts STAN (DE11) from parsed response. """ def get_stan(parsed) do get_field(parsed, 11) end end